Compare commits

...

24 Commits

Author SHA1 Message Date
9e8cfe793c Update actions.py 2024-12-01 04:57:57 +08:00
b9a2b3bc31 1
1
2024-12-01 04:48:55 +08:00
af5721992d Update README.md 2024-12-01 04:19:14 +08:00
672c3ad43e 1
1
2024-12-01 03:53:46 +08:00
15eaaa42b6 1
1
2024-12-01 03:47:31 +08:00
b54b46ca11 1
1
2024-12-01 03:35:58 +08:00
5e4f49edbb 1 2024-12-01 03:06:12 +08:00
91ac18c8e6 Update utils.py 2024-12-01 03:00:26 +08:00
29592cd05b 1 2024-12-01 02:58:09 +08:00
227800f0e8 Update calculate_fan.py 2024-12-01 02:12:22 +08:00
1562f784bf Update test_chengdu_mahjong_states.py 2024-12-01 02:05:48 +08:00
3d87d3850f Update test_chengdu_mahjong_states.py 2024-12-01 01:59:47 +08:00
729f1bf7a1 1
1
2024-12-01 01:51:51 +08:00
fbfb4b89e1 1
1
2024-12-01 00:55:39 +08:00
06accd20dd Update test_game_status.py 2024-12-01 00:41:37 +08:00
b17e775163 Update chengdu_mahjong_state.py 2024-12-01 00:41:35 +08:00
8a966890d5 1
1
2024-12-01 00:34:47 +08:00
33b1cc761a Update test_game_status.py 2024-11-30 23:47:24 +08:00
5f254e406d Update chengdu_mahjong_state.py 2024-11-30 23:47:21 +08:00
4bf51b919b Update test_game_status.py 2024-11-30 23:35:45 +08:00
ccbe55dbc7 1 2024-11-30 23:26:13 +08:00
c3dc6cf234 Update README.md 2024-11-30 23:12:33 +08:00
f8fadb435c Update game_state.py 2024-11-30 22:47:44 +08:00
b2d9514d2a 1
1
2024-11-30 22:44:50 +08:00
17 changed files with 1621 additions and 431 deletions

View File

@@ -77,7 +77,7 @@
- **定缺**:游戏开始时,每位玩家需要扣下一张牌作为自己缺的那门,并且不能更改。如果本身就是两门牌,则可以报“天缺”而不扣牌。
- **起牌与打牌**:庄家通过掷骰子决定起牌位置,然后按顺序抓牌。庄家先出牌,之后每家依次摸牌打牌。
- **碰、杠**:允许碰牌和杠牌,但不允许吃牌。杠牌分为明杠和暗杠,明杠是其他玩家打出的牌被你碰后又摸到相同的牌;暗杠则是你自己摸到四张相同的牌。
- **胡牌**:胡牌的基本条件是拥有一个对子加上四个顺子或刻子(三个相同牌)。自摸为三家给分,点炮则由放炮者给分。
- **胡牌**:胡牌的基本条件是拥有一个对子加上四个顺子或刻子(三个相同牌)。自摸为三家给分,点炮则由放炮者给分。n*AAA+m*ABC+DD mn可以等于0。
- **血战到底**:一家胡牌后,其他未胡牌的玩家继续游戏,直到只剩下最后一位玩家或者黄庄(所有牌都被摸完)为止。
### 特殊规则
@@ -91,45 +91,55 @@
#### 详细番数计算
1. **平胡(基本胡)**四坎牌加一对将四坎牌可以是刻子或顺子计为1番。
**平胡(基本胡)**四坎牌加一对将四坎牌可以是刻子或顺子计为1番。
2. **清一色**
**带根** 在玩家胡牌的手牌当中,有四张牌是一摸一样的,这样的牌就叫做带根。牌型如:一二三,三三三,万。四五六,五六七,九九条。
- 不带杠的清一色称为“素清”计为2番
- 带杠的清一色或清一色对子胡简称“清对”计为3番称为“极品”。
- 带两杠的清一色或清一色对子胡带杠计为4番称为“极中极”或“精品”。<!--存疑-->
**对子胡**玩家的手牌除了一对将牌以外剩下的牌都是三张一样的一共四对这样的牌型胡牌就叫做对子胡。计1番。牌型如一一一三三三四四四万六六六七七筒
3. **带幺九**
**清一色**
- **带幺九**指玩家手上的牌全部是由1和9组成的顺子、刻子或对子。例如123, 789, 111, 999, 11等。计为3番。
不带杠的清一色称为“素清”,计为2番。
- **清带幺九**指玩家手上的牌不仅全部由1和9组成而且是同一花色条、筒、万即清一色的带幺九。计为1番。<!--存疑-->
带杠的清一色或清一色对子胡简称“清对”计为3番称为“极品”。
4. **七对**手牌由7个对子组成计为2番。
带两杠的清一色或清一色对子胡带杠计为4番称为“极中极”或“精品”。<!--存疑-->
5. **全求人**所有牌都是通过碰、杠、吃别人打出的牌来完成的计为6番
**将对** 玩家手上的牌是带二、五、八的对对胡这样的牌型叫将对。计3番。牌型如二二二五五五八八八万五五五八八筒
6. **七对**七对中有一对是三张相同的牌计为12番
**七对**玩家胡牌的手牌全部都是两张一对的没有碰过牌和杠过牌。这样的牌型叫做七对或暗七对。计2番。牌型如一一三三四四六六万五五七七九九筒
7. **清七对**全部由一种花色组成的七对计为12番
**清七对**玩家手上的牌是清一色的七对这样的牌型叫清七对。计4番。牌型如一一三三四四六六七七八八九九万
8. **杠上开花**在杠牌之后立即自摸胡牌计为1番
**龙七对**玩家手中的牌为暗七对牌型没有碰过牌杠过牌时并且有四张牌是一模一样的这样的牌型叫做龙七对。计为4番。牌型如一一二二二二四四万 五五七七八八筒
9. **抢杠胡**当其他玩家明杠时你正好可以胡那张牌计为1番
**清龙七对**玩家手上的牌是清一色的龙七对这样的牌型叫清龙七对。计5番。牌型如一一二二四四四四五五七七九九万
10. **天胡**庄家起牌后直接胡牌计为12番。
**带幺九**
11. **地胡**:闲家在第一轮打牌时就胡牌,计为12番。
- **带幺九**指玩家手上的牌全部是由1和9组成的顺子、刻子或对子。例如123, 789, 111, 999, 11等。计为2番。
- **清带幺九**指玩家手上的牌不仅全部由1和9组成而且是同一花色条、筒、万即清一色的带幺九。计为4番。
12. **大对子**手牌由四个对子加一个刻子组成计为2番。
**杠上开花**另外再单独计算带根的番数玩家的手牌已经下叫并且玩家在杠牌时杠起一张牌正好是玩家自己所要的叫牌这时玩家可以选择胡牌这种情况叫做杠上花。计为1番。每杠加1番。
13. **小七对**有六对加上一个对子计为2番。
**杠上炮**玩家在杠牌时先杠一张牌再打掉一张牌而打出的这张牌正好是其它玩家胡牌所需要的叫牌时这种情况叫做杠上炮。每杠加1番。
14. **金钩吊**手上只剩下一张牌等别人打出然后胡牌计为1番。
**报叫**庄家在配牌完成后打出第一张牌就下叫或者闲家在配牌完成后就下叫并宣告下叫庄家闲家都要之后不等变更手牌。计2番。
15. **海底捞月**:最后一张牌被玩家摸到并胡牌,计为1番。
**天胡**:庄家起牌后直接胡牌,计为5番。
16. **海底炮**:最后一张牌被打出,导致玩家胡牌,计为1番。
**地胡**:闲家在第一轮打牌时就胡牌,计为5番。
#### 不确定是否有的类型
1. **全求人**所有牌都是通过碰、杠、吃别人打出的牌来完成的计为3番。
1. **抢杠胡**当其他玩家明杠时你正好可以胡那张牌计为1番。
2. **大对子**手牌由四个对子加一个刻子组成计为2番。
3. **小七对**有六对加上一个对子计为2番。
4. **金钩吊**手上只剩下一张牌等别人打出然后胡牌计为1番。
5. **海底捞月**最后一张牌被玩家摸到并胡牌计为1番。
6. **海底炮**最后一张牌被打出导致玩家胡牌计为1番。
这些番数可以叠加例如如果一个玩家同时满足了清一色和七对那么他的总番数就是2番清一色+ 2番七对= 4番。
@@ -140,6 +150,28 @@
**自摸 **:是指玩家通过自己摸牌完成胡牌。自摸时,其他玩家都需要给赢家支付相应的分数。
## 成都麻将游戏流程
1.确定庄家:通常在第一局开始时通过掷骰子来决定庄家。以后每局由上一局胡牌的玩家坐庄,如果流局则庄家不变。
2.庄家摸牌从掷骰子确定的位置开始庄家先摸14张牌其他玩家每人摸13张牌。
3.庄家出牌:庄家先打出一张牌,开始这一局的游戏。
4.顺时针出牌:接下来按照顺时针方向,每位玩家依次摸牌和出牌。
5.摸牌与出牌:每个玩家轮流摸一张牌,然后选择出一张牌。玩家可以进行碰、杠等操作。
6.打缺一门:玩家必须选择先打完定缺的花色牌,才能出其他牌。(缺一种花色(筒、条、万中的一种),即手牌中只能保留两种花色)
7.自摸:玩家摸到的牌使自己胡牌。
8.点炮:其他玩家打出的牌使自己胡牌。
9.计分:胡牌后根据胡牌的番数和其他规则进行计分。)
10.结算
## 成都麻将规则建模
麻将游戏引擎建模代码于项目根src/engine/目录下。

View File

@@ -1,45 +1,85 @@
from loguru import logger
from src.engine.utils import get_tile_name
from src.engine.mahjong_tile import MahjongTile
def draw_tile(engine):
"""
当前玩家摸牌逻辑,记录牌的详细信息和游戏状态。
"""
# 检查牌堆是否已空
if engine.state.remaining_tiles == 0:
logger.warning("牌堆已空,游戏结束!")
engine.game_over = True
return 0, True # 游戏结束时返回 0 和 done = True
tile = engine.state.deck.pop(0) # 从牌堆中取出一张牌
engine.state.remaining_tiles -= 1 # 更新剩余牌数
engine.state.hands[engine.state.current_player][tile] += 1 # 加入当前玩家手牌
# 当前玩家
current_player = engine.state.current_player
tile_name = get_tile_name(tile) # 获取具体的牌名
# 从牌堆中摸一张牌
tile = engine.state.deck.pop(0) # 从牌堆抽取一张牌
engine.state.remaining_tiles -= 1 # 更新剩余牌数
engine.state.hands[current_player].add_tile(tile) # 将牌加入当前玩家手牌
# 获取牌名
tile_name = str(tile) # 调用 MahjongTile 的 __repr__ 方法
logger.info(
f"玩家 {engine.state.current_player} 摸到一张牌: {tile_name}(索引 {tile})。剩余牌堆数量: {engine.state.remaining_tiles}"
f"玩家 {current_player} 摸到一张牌: {tile_name}(索引 {tile})。"
f"剩余牌堆数量: {engine.state.remaining_tiles}"
)
# 检查摸到的牌是否属于缺门
missing_suit = engine.state.missing_suits[current_player]
if tile.suit == missing_suit:
logger.info(f"玩家 {current_player} 摸到缺门牌: {tile_name},需要优先打出")
# 切换到下一位玩家
next_player = (current_player + 1) % 4
engine.state.current_player = next_player
# 返回奖励和游戏是否结束的标志
return 0, False # 奖励为 0done 为 False游戏继续
def discard_tile(self, tile):
"""
当前玩家打牌逻辑,记录打出的牌和当前牌河信息。
"""
if self.state.hands[self.state.current_player][tile] == 0:
logger.error(f"玩家 {self.state.current_player} 尝试打出不存在的牌: 索引 {tile}")
current_player = self.state.current_player
hand = self.state.hands[current_player]
# 检查牌的有效性
if not isinstance(tile, MahjongTile):
logger.error(f"玩家 {current_player} 尝试打出无效的牌: {tile}")
raise ValueError("打出的牌必须是 MahjongTile 对象")
# 检查是否有这张牌
if hand[tile] == 0:
logger.error(f"玩家 {current_player} 尝试打出不存在的牌: {tile}")
raise ValueError("玩家没有这张牌")
self.state.hands[self.state.current_player][tile] -= 1 # 从手牌中移除
self.state.discards[self.state.current_player].append(tile) # 加入牌河
# 检查缺门规则
missing_suit = self.state.missing_suits[current_player]
if tile.suit == missing_suit and any(t.suit == missing_suit for t in hand.tiles):
logger.error(f"玩家 {current_player} 尝试打出非缺门的牌: {tile}")
raise ValueError("必须先打完缺门花色的牌")
tile_name = get_tile_name(tile) # 获取具体的牌名
# 从手牌中移除
hand[tile] -= 1
self.state.discards[current_player].append(tile)
# 打出的牌名
tile_name = get_tile_name(tile)
# 打出牌后打印状态
logger.info(
f"玩家 {self.state.current_player} 打出一张牌: {tile_name}(索引 {tile})。当前牌河: {[get_tile_name(t) for t in self.state.discards[self.state.current_player]]}"
f"玩家 {current_player} 打出一张牌: {tile_name}(索引 {tile})。"
f"当前牌河: {[get_tile_name(t) for t in self.state.discards[current_player]]}"
)
# 检查是否触发其他玩家的操作(碰、杠、胡牌)
self.check_other_players(tile)
return tile
def peng(self, tile):
"""
@@ -97,28 +137,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

View File

@@ -1,98 +1,92 @@
def calculate_fan(hand, melds, is_self_draw, is_cleared, conditions):
"""
根据规则动态计算番数。
动态计算番数,优先处理高番数。
参数:
- hand: 当前胡牌的手牌(长度为108的列表表示每张牌的数量)。
- melds: 碰杠等明牌列表。
- hand: 当前胡牌的手牌(Hand 对象)。
- melds: 玩家已明牌列表Meld 对象列表)
- is_self_draw: 是否自摸。
- is_cleared: 是否清一色。
- conditions: 其他胡牌条件的字典,例{'is_seven_pairs': True, 'add_self_draw': True}。
- conditions: 字典,包含特殊胡牌条件,{"is_seven_pairs": True, "is_tian_hu": True}。
返回:
- fan: 番数。
- fan: 最大番数。
"""
fan = 0 # 初始番数
fan = 0 # 默认番数
# 定义番规则
# 定义番规则(按优先级从高到低排序)
rules = {
"is_pure_cleared": lambda: 3 if is_cleared and len(melds) == 3 and not conditions.get("is_double_pure_cleared",
False) else 0,
"is_double_pure_cleared": lambda: 4 if is_cleared and len(melds) >= 2 and conditions.get(
"is_double_pure_cleared", False) else 0,
"is_cleared": lambda: 2 if is_cleared and not (conditions.get("is_pure_cleared", False) or
conditions.get("is_double_pure_cleared", False) or
conditions.get("is_clear_seven_pairs", False)) else 0,
"is_seven_pairs": lambda: 2 if conditions.get("is_seven_pairs", False) and not conditions.get(
"is_dragon_seven_pairs", False) else 0,
"is_dragon_seven_pairs": lambda: 12 if conditions.get("is_dragon_seven_pairs", False) else 0,
"is_clear_seven_pairs": lambda: 12 if conditions.get("is_clear_seven_pairs", False) else 0,
"is_big_pairs": lambda: 2 if conditions.get("is_big_pairs", False) else 0,
"is_small_pairs": lambda: 2 if conditions.get("is_small_pairs", False) else 0,
"is_full_request": lambda: 6 if conditions.get("is_full_request", False) else 0,
"is_gang_flower": lambda: 1 if conditions.get("is_gang_flower", False) else 0,
"is_rob_gang": lambda: 1 if conditions.get("is_rob_gang", False) else 0,
"is_under_the_sea": lambda: 1 if conditions.get("is_under_the_sea", False) else 0,
"is_tian_hu": lambda: 12 if conditions.get("is_tian_hu", False) else 0,
"is_di_hu": lambda: 12 if conditions.get("is_di_hu", False) else 0,
"basic_win": lambda: 1 if not (conditions.get("is_seven_pairs", False) or
conditions.get("is_big_pairs", False) or
conditions.get("is_dragon_seven_pairs", False) or
conditions.get("is_pure_cleared", False) or
conditions.get("is_double_pure_cleared", False) or
conditions.get("is_small_pairs", False) or
conditions.get("is_clear_seven_pairs", False) or
conditions.get("is_full_request", False) or
conditions.get("is_rob_gang", False) or
conditions.get("is_under_the_sea", False) or
conditions.get("is_tian_hu", False) or
conditions.get("is_di_hu", False)) else 0,
"tian_hu": lambda: 12 if conditions.get("is_tian_hu", False) else 0, # 天胡
"di_hu": lambda: 12 if conditions.get("is_di_hu", False) else 0, # 地胡
"dragon_seven_pairs": lambda: 12 if is_dragon_seven_pairs(hand) else 0, # 龙七对
"clear_seven_pairs": lambda: 12 if is_cleared and is_seven_pairs(hand) else 0, # 清七对
"full_request": lambda: 6 if is_full_request(hand, melds) else 0, # 全求人
"pure_cleared": lambda: 4 if is_cleared and len(melds) >= 2 else 0, # 极品清一色
"seven_pairs": lambda: 2 if is_seven_pairs(hand) else 0, # 七对
"big_pairs": lambda: 2 if is_big_pairs(hand) else 0, # 大对子
"small_pairs": lambda: 2 if is_small_pairs(hand) else 0, # 小七对
"golden_hook": lambda: 1 if is_golden_hook(hand) else 0, # 金钩吊
"gang_flower": lambda: 1 if conditions.get("is_gang_flower", False) else 0, # 杠上开花
"rob_gang": lambda: 1 if conditions.get("is_rob_gang", False) else 0, # 抢杠胡
"under_the_sea": lambda: 1 if conditions.get("is_under_the_sea", False) else 0, # 海底捞月
"plain_win": lambda: 1 if not any(conditions.values()) else 0 # 平胡
}
print("\nCalculating fan...")
# 逐一应用规则
for rule, func in rules.items():
result = func()
fan += result
print(f"Rule: {rule}, Fan: {result}") # 调试输出
if result > fan:
fan = result # 选择当前最大番数
return fan
# 辅助函数实现
def is_seven_pairs(hand):
"""
检查手牌是否是七对。
"""
return sum(1 for count in hand if count == 2) == 7
"""检查是否是七对(七个对子)。"""
from collections import Counter
counter = Counter(hand.tiles)
return sum(1 for count in counter.values() if count == 2) == 7
def is_dragon_seven_pairs(hand):
"""检查是否是龙七对(七对中有一对是三张)。"""
from collections import Counter
counter = Counter(hand.tiles)
pairs = sum(1 for count in counter.values() if count == 2)
triplets = sum(1 for count in counter.values() if count == 3)
return pairs == 6 and triplets == 1
def is_big_pairs(hand):
"""检查是否是大对子(四坎牌和一对将,坎牌为刻子)。"""
from collections import Counter
counter = Counter(hand.tiles)
triplets = sum(1 for count in counter.values() if count == 3)
pairs = sum(1 for count in counter.values() if count == 2)
return triplets == 4 and pairs == 1
def is_golden_hook(hand):
"""检查是否是金钩吊(只剩一张牌)。"""
return len(hand.tiles) == 1
def is_full_request(hand, melds):
"""检查是否是全求人(所有牌通过碰、杠完成)。"""
return all(tile_count == 0 for tile_count in hand.tiles) and len(melds) > 0
def is_cleared(hand, melds):
"""检查是否是清一色(所有牌同一种花色)。"""
all_tiles = hand.tiles + [meld.tile for meld in melds]
suits = {tile.suit for tile in all_tiles}
return len(suits) == 1
def is_small_pairs(hand):
"""
检查手牌和明牌是否是清一色
参数:
- hand: 当前胡牌的手牌长度为108的列表表示每张牌的数量
- melds: 碰杠等明牌列表。
返回:
- bool: 是否为清一色。
"""
# 获取所有牌的花色
all_tiles = hand + [tile for meld in melds for tile in meld]
suits = [tile // 36 for tile in all_tiles if tile > 0]
# 检查是否有多种花色
return len(set(suits)) == 1
def is_big_pairs(hand):
"""
检查手牌是否是大对子(由刻子和一对组成)。
检查是否是小七对(六对加一对)
"""
from collections import Counter
counter = Counter(hand)
counts = [count for count in hand if count > 0]
result = counts.count(2) == 1 and counts.count(3) >= 3
print(f"Big pairs check: {result}, Counter: {Counter(counts)}")
return result
counter = Counter(hand.tiles)
pairs = sum(1 for count in counter.values() if count == 2)
return pairs == 6

View File

@@ -1,47 +1,99 @@
import random
from loguru import logger
from .game_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("游戏已结束")

View File

@@ -0,0 +1,138 @@
from collections import Counter
from src.engine.hand import Hand
from src.engine.mahjong_tile import MahjongTile
from src.engine.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.scores = [100, 100, 100, 100]
# 剩余牌数量
self.remaining_tiles = len(self.deck)
# 胜利玩家列表
self.winners = []
# 缺门信息
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

167
src/engine/fan_type.py Normal file
View File

@@ -0,0 +1,167 @@
from src.engine.utils import try_win,is_terminal_tile
from collections import Counter
def is_basic_win(hand):
# 将手牌转换为列表并按花色和数值排序
all_tiles = hand.tiles[:]
all_tiles.sort(key=lambda t: (t.suit, t.value))
# 调用递归函数检查是否符合平胡
if try_win(all_tiles):
return 1
return 0
def is_cleared(hand, melds):
# 合并所有牌(手牌和明牌)
all_tiles = hand.tiles[:]
for meld in melds:
if meld.is_triplet():
all_tiles.extend([meld.tile] * 3)
elif meld.is_kong():
all_tiles.extend([meld.tile] * 4)
# 检查是否只有一种花色
suits = {tile.suit for tile in all_tiles}
if len(suits) != 1:
return 0 # 不是清一色
# 计算杠的数量
gang_count = sum(1 for meld in melds if meld.is_kong())
# 检查是否符合基本胡规则(四坎牌加一对将)
if try_win(hand.tiles):
if gang_count == 0:
return 2 # 素清
elif gang_count == 1:
return 3 # 极品
elif gang_count >= 2:
return 4 # 极中极
return 0
def calculate_terminal_fan(hand, melds):
"""
计算带幺九番型,并返回对应番数。
"""
# 合并所有牌(手牌和明牌)
all_tiles = hand.tiles[:]
for meld in melds:
if meld.is_triplet():
all_tiles.extend([meld.tile] * 3)
elif meld.is_kong():
all_tiles.extend([meld.tile] * 4)
# 检查是否同时包含 1 和 9
contains_one = any(tile.value == 1 for tile in all_tiles)
contains_nine = any(tile.value == 9 for tile in all_tiles)
if not (contains_one and contains_nine):
return 0 # 不符合带幺九
# 检查是否符合花色限制
suits = {tile.suit for tile in all_tiles}
if len(suits) > 2:
return 0 # 不符合花色要求
# 检查是否符合基本胡规则(四坎牌加一对将)
sorted_tiles = sorted(all_tiles, key=lambda t: (t.suit, t.value))
if try_win(sorted_tiles):
return 3 # 带幺九
return 0
def is_seven_pairs(hand):
"""
判断是否符合七对番型。
七对要求:手牌由 7 个对子组成。
参数:
- hand: Hand 对象,表示玩家当前的手牌。
返回:
- int: 如果符合七对,返回 2番数否则返回 0。
"""
# 获取手牌的计数
tile_counts = hand.tile_count
# 统计对子数量
pairs_count = sum(1 for count in tile_counts.values() if count == 2)
# 检查是否有 7 个对子
if pairs_count == 7:
return 2 # 七对计为 2 番
return 0
def is_full_request(hand, melds, winning_tile):
"""
判断是否符合全求人番型。
全求人要求:
- 玩家所有牌都通过碰、杠、吃完成。
- 玩家手上只剩下 1 张牌。
- 胡牌必须通过其他玩家打出的牌。
参数:
- hand: Hand 对象,表示玩家当前的手牌。
- melds: list[Meld] 对象,表示碰、杠等明牌。
- winning_tile: MahjongTile 对象,表示胡的那张牌。
返回:
- int: 如果符合全求人,返回 6番数否则返回 0。
"""
# 检查手牌中是否只剩下 1 张牌
if len(hand.tiles) != 1:
return 0
# 检查手中剩余的这张牌是否是胡牌
if hand.tiles[0] != winning_tile:
return 0
# 检查是否有明牌(碰、杠、吃),且满足全求人条件
if not melds or not all(meld.is_triplet() or meld.is_kong() or meld.is_sequence() for meld in melds):
return 0
# 符合全求人
return 6
def is_dragon_seven_pairs(hand, melds):
"""
判断是否符合龙七对的番型,并返回番数和剩余根数。
条件:
- 玩家手牌为七对14张包含7个对子
- 没有碰过或者杠过牌melds为空
- 至少一个对子升级为四张牌。
参数:
- hand: Hand 对象,表示玩家当前的手牌。
- melds: 明牌列表(碰、杠等),必须为空。
返回:
- (int, int): 如果符合龙七对,返回 (12, -1) 表示 12 番和减去 1 根;否则返回 (0, 0)。
"""
if melds: # 如果有明牌(碰或杠),不符合条件
return 0, 0
# 获取手牌中每张牌的数量
tile_counts = Counter(hand.tiles)
# 统计对子和四张牌的数量
pairs_count = 0
four_of_a_kind_found = False
for count in tile_counts.values():
if count == 2:
pairs_count += 1
elif count == 4:
four_of_a_kind_found = True
pairs_count += 1
# 检查是否符合龙七对的条件
if pairs_count == 7 and four_of_a_kind_found:
return 12, -1 # 龙七对计为 12 番,并减少 1 根
return 0, 0

View File

@@ -1,89 +0,0 @@
from .utils import get_suit,get_tile_name
from loguru import logger
class ChengduMahjongState:
def __init__(self):
# 每个玩家的手牌使用108个索引表示
self.hands = [[0] * 108 for _ in range(4)] # 每个玩家108张牌的计数
# 每个玩家的打出的牌
self.discards = [[] for _ in range(4)] # 每个玩家的弃牌列表
# 每个玩家的明牌(碰、杠)
self.melds = [[] for _ in range(4)]
# 剩余的牌堆
self.deck = list(range(108)) # 0-107 表示108张牌
# 当前玩家索引
self.current_player = 0
# 玩家分数
self.scores = [100, 100, 100, 100]
# 剩余牌数量
self.remaining_tiles = 108
# 胜利玩家列表
self.winners = []
# 缺门信息
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 can_win(self, hand):
"""
判断是否满足胡牌条件:四组(顺子或刻子)+ 一对将。
"""
from collections import Counter
def is_valid_group(tiles):
"""
判断是否为合法的顺子或刻子。
"""
if len(tiles) != 3:
return False
tiles.sort() # 确保顺子检查按顺序排列
return (tiles[0] == tiles[1] == tiles[2]) or \
(tiles[0] + 1 == tiles[1] and tiles[1] + 1 == tiles[2])
def try_win(remaining_tiles, depth=0):
"""
递归检查是否可以将剩余牌分为合法组合。
"""
if not remaining_tiles:
return depth == 4 # 必须分成四组
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:]
# 确保顺子检查按顺序排列
next_tiles.sort()
if try_win(next_tiles, depth + 1):
return True
return False
counter = Counter({tile: count for tile, count in enumerate(hand) if count > 0})
pairs = [tile for tile, count in counter.items() if count >= 2]
for pair in pairs:
temp_hand = hand[:]
temp_hand[pair] -= 2 # 移除将牌
remaining_tiles = [tile for tile, count in enumerate(temp_hand) for _ in range(count)]
remaining_tiles.sort() # 确保顺子检查按顺序排列
if try_win(remaining_tiles):
return True
return False

View File

@@ -1,21 +1,24 @@
from collections import defaultdict
from src.engine.mahjong_tile import MahjongTile
from collections import defaultdict
class Hand:
def __init__(self):
# 存储所有的
# 存储所有的 MahjongTile 对象
self.tiles = []
# 存储每种牌的数量,默认值为 0
# 存储每种牌的数量,键为 MahjongTile 对象,值为数量
self.tile_count = defaultdict(int)
def add_tile(self, tile):
""" 向手牌中添加一张牌 """
if not isinstance(tile, MahjongTile):
raise ValueError("必须添加 MahjongTile 类型的牌")
self.tiles.append(tile) # 将牌添加到手牌中
self.tile_count[tile] += 1 # 增加牌的数量
def remove_tile(self, tile):
""" 从手牌中移除一张牌 """
if not isinstance(tile, MahjongTile):
raise ValueError("必须移除 MahjongTile 类型的牌")
if self.tile_count[tile] > 0:
self.tiles.remove(tile)
self.tile_count[tile] -= 1
@@ -24,16 +27,23 @@ class Hand:
def get_tile_count(self, tile):
""" 获取手牌中某张牌的数量 """
if not isinstance(tile, MahjongTile):
raise ValueError("必须是 MahjongTile 类型的牌")
return self.tile_count[tile]
def can_peng(self, tile):
""" 判断是否可以碰即是否已经有2张相同的牌摸一张牌后可以碰 """
if not isinstance(tile, MahjongTile):
raise ValueError("必须是 MahjongTile 类型的牌")
return self.tile_count[tile] == 2 # 摸一张牌后总数为 3 张,才可以碰
def can_gang(self, tile):
""" 判断是否可以杠即是否已经有3张相同的牌摸一张牌后可以杠 """
return self.tile_count[tile] == 3 # 摸一张牌后总数为 4 张,才可以杠
if not isinstance(tile, MahjongTile):
raise ValueError("必须是 MahjongTile 类型的牌")
return self.tile_count[tile] == 4 # 摸一张牌后总数为 4 张,才可以杠
def __repr__(self):
""" 返回手牌的字符串表示 """
return f"手牌: {self.tiles}, 牌的数量: {dict(self.tile_count)}"
tiles_str = ", ".join(str(tile) for tile in self.tiles)
return f"手牌: [{tiles_str}], 牌的数量: {dict(self.tile_count)}"

37
src/engine/meld.py Normal file
View File

@@ -0,0 +1,37 @@
from src.engine.mahjong_tile import MahjongTile
class Meld:
def __init__(self, tile, type: str):
"""
初始化一个碰或杠的对象。
:param tile: MahjongTile 对象,表示碰或杠的牌。
:param type: 字符串,'''',表示碰或杠。
"""
if not isinstance(tile, MahjongTile):
raise TypeError("tile 必须是 MahjongTile 类型")
if type not in ['', '']:
raise ValueError("type 必须是 ''''")
self.tile = tile
self.type = type
self.count = 3 if type == '' else 4 # 碰为3张杠为4张
def __repr__(self):
return f"({self.type}: {self.tile} x{self.count})"
def __eq__(self, other):
if not isinstance(other, Meld):
return False
return self.tile == other.tile and self.type == other.type
def __hash__(self):
return hash((self.tile, self.type))
def is_triplet(self):
"""是否为碰"""
return self.type == ''
def is_kong(self):
"""是否为杠"""
return self.type == ''

View File

@@ -1,19 +1,54 @@
def get_suit(tile_index):
"""
根据牌的索引返回花色。
"""
if 0 <= tile_index <= 35:
return ""
elif 36 <= tile_index <= 71:
return ""
elif 72 <= tile_index <= 107:
return ""
else:
raise ValueError(f"无效的牌索引: {tile_index}")
from collections import Counter
def get_tile_name(tile_index):
def is_valid_group(tiles):
"""
根据牌的索引返回牌名例如1条2筒等)。
检查是否是合法组AAA 或 ABC)。
"""
suit = get_suit(tile_index)
return f"{tile_index % 36 + 1}{suit}"
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
def is_terminal_tile(tile):
"""
检查单张牌是否是幺九牌1 或 9
"""
return tile.value in {1, 9}

View File

@@ -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
}

40
test.py
View File

@@ -1,3 +1,37 @@
import torch
print(torch.cuda.is_available()) # 如果返回True说明可以使用GPU
print(torch.__version__)
from src.engine.chengdu_mahjong_state import ChengduMahjongState
from src.engine.hand import Hand
from src.engine.mahjong_tile import MahjongTile
from src.engine.meld import Meld
hand = Hand()
# 添加暗牌
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 8))
# 添加对子
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 设置明牌(杠)
melds_list = [
Meld(MahjongTile("", 9), "") # 表示明杠了4张9筒
]
state.melds[0] = melds_list # 确保 state.melds[0] 是一个列表
# 设置缺门为 "万"
missing_suit = ""
print(f"\n当前手牌: {state.hands[0]}, 明牌: {state.melds[0]}")
print(f"是否可以胡: {state.can_win(state.hands[0], state.melds[0], missing_suit)}")

View File

@@ -1,31 +1,36 @@
import pytest
from src.engine.calculate_fan import calculate_fan, is_seven_pairs, is_cleared, is_big_pairs
from src.engine.hand import Hand
from src.engine.mahjong_tile import MahjongTile
# 测试用例
def test_basic_win():
"""
测试平胡(基本胡)计分
"""
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 = Hand()
# 模拟平胡手牌四组顺子 + 一对将
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1)) #
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3)) # 顺子
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6)) # 顺子
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9)) # 顺子
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3)) # 顺子
melds = []
conditions = {}
fan = calculate_fan(hand, melds, is_self_draw=False, is_cleared=False, conditions=conditions)
fan = calculate_fan(hand.tiles, melds, is_self_draw=False, is_cleared=False, conditions=conditions)
assert fan == 1, f"Expected 1 fan, got {fan}"
@@ -265,3 +270,4 @@ def test_self_draw():
fan = calculate_fan(hand, melds, is_self_draw=True, is_cleared=False, conditions=conditions)
assert fan == 1, f"Expected 1 fan (self-draw), got {fan}"

View File

@@ -27,7 +27,7 @@ def test_discard_tile():
def test_set_missing_suit():
from src.engine.game_state import ChengduMahjongState
from src.engine.chengdu_mahjong_state import ChengduMahjongState
state = ChengduMahjongState()
player = 0
@@ -41,7 +41,7 @@ def test_set_missing_suit():
def test_can_win():
from src.engine.game_state import ChengduMahjongState
from src.engine.chengdu_mahjong_state import ChengduMahjongState
state = ChengduMahjongState()
hand = [0] * 108

View File

@@ -0,0 +1,514 @@
from src.engine.chengdu_mahjong_state import ChengduMahjongState
from src.engine.hand import Hand
from src.engine.mahjong_tile import MahjongTile
from src.engine.meld import Meld
def test_set_missing_suit():
"""测试设置缺门功能"""
state = ChengduMahjongState()
state.set_missing_suit(0, "")
assert state.missing_suits[0] == "", "测试失败:缺门设置为 '' 后未正确更新"
state.set_missing_suit(1, "")
assert state.missing_suits[1] == "", "测试失败:缺门设置为 '' 后未正确更新"
state.set_missing_suit(2, "")
assert state.missing_suits[2] == "", "测试失败:缺门设置为 '' 后未正确更新"
try:
state.set_missing_suit(0, "")
except ValueError:
print("测试通过:设置无效缺门 '' 抛出异常")
else:
raise AssertionError("测试失败:设置无效缺门 '' 未抛出异常")
def test_can_win_with_pure_sequences():
"""测试纯顺子胡牌"""
hand = Hand()
# 添加牌到手牌中
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
state = ChengduMahjongState()
state.melds[0] = []
state.hands[0] = hand
# 设置缺门为 "条",因为手牌中没有 "条"
missing_suit = ""
print(f"\n,state.hand[0]: {state.hands[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0],missing_suit) == True, "测试失败:纯顺子应该可以胡牌"
def test_can_win_with_sequence_and_triplet():
"""测试顺子 + 刻子胡牌"""
hand = Hand()
# 添加牌到手牌中
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
state = ChengduMahjongState()
state.hands[0] = hand
state.melds[0] = []
# 设置缺门为 "条",因为手牌中没有 "条"
missing_suit = ""
print(f"\n,state.hand[0]: {state.hands[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0],missing_suit) == True, "测试失败:顺子 + 刻子应该可以胡牌"
def test_can_win_with_triplets_and_pair():
"""测试刻子和对子胡牌"""
hand = Hand()
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
state = ChengduMahjongState()
state.hands[0] = hand
state.melds[0] = []
# 设置缺门为 "万",因为手牌中没有 "万"
missing_suit = ""
print(f"\n,state.hand[0]: {state.hands[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0],missing_suit) == True, "测试失败:刻子和对子应该可以胡牌"
def test_can_win_with_pure_one_suit():
"""测试清一色不带杠胡牌"""
hand = Hand()
# 添加牌到手牌中
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
state = ChengduMahjongState()
state.hands[0] = hand
state.melds[0] = []
# 设置缺门为 "万",因为手牌中只有 "筒"
missing_suit = ""
print(f"\n,state.hand[0]: {state.hands[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0],missing_suit) == True, "测试失败:清一色不带杠应该可以胡牌"
def test_can_win_with_pure_one_suit_and_gang():
"""测试带杠的清一色胡牌"""
hand = Hand()
# 添加暗牌
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 8))
# 添加对子
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 设置明牌(杠)
melds_list = [
Meld(MahjongTile("", 9), "") # 表示明杠了4张9筒
]
state.melds[0] = melds_list # 确保 state.melds[0] 是一个列表
# 设置缺门为 "万"
missing_suit = ""
print(f"\n当前手牌: {state.hands[0]}, 明牌: {state.melds[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:带杠的清一色应该可以胡牌"
def test_can_win_with_yaojiu_sequences():
"""测试带幺九的顺子胡牌"""
hand = Hand()
# 添加顺子和对子
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
# 添加对子
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 无明牌
melds_list = []
state.melds[0] = melds_list
# 设置缺门为 "万"
missing_suit = ""
print(f"\n当前手牌: {state.hands[0]}, 明牌: {state.melds[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:带幺九顺子应该可以胡牌"
def test_can_win_with_seven_pairs():
"""测试清一色七对胡牌"""
hand = Hand()
# 添加 7 对牌
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 无明牌
melds_list = []
state.melds[0] = melds_list
# 设置缺门为 "条"
missing_suit = ""
print(f"\n当前手牌: {state.hands[0]}, 明牌: {state.melds[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:七对应该可以胡牌"
def test_can_win_with_dragon_seven_pairs():
"""测试带暗杠的龙七对胡牌"""
hand = Hand()
# 添加手牌
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1)) # 暗杠
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 无明牌
melds_list = []
state.melds[0] = melds_list
# 设置缺门为 "万"
missing_suit = ""
print(f"\n当前手牌: {state.hands[0]}, 明牌: {state.melds[0]}")
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:带暗杠的龙七对应该可以胡牌"
def test_can_win_with_mixed_seven_pairs():
"""测试混合七对胡牌"""
hand = Hand()
# 添加手牌
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 无明牌
melds_list = []
state.melds[0] = melds_list
# 设置缺门为 "万"
missing_suit = ""
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:混合七对应该可以胡牌"
def test_can_win_after_ming_gang():
"""测试明杠后杠上开花胡牌"""
hand = Hand()
# 添加暗牌
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 2))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 设置明牌(明杠)
melds_list = []
state.melds[0] = melds_list
# 别人打出3条明杠了3条
state.hands[0].add_tile(MahjongTile("", 3))
# 设置明杠
state.melds[0].append(Meld(MahjongTile("", 3), ""))
# 从手牌中移除明杠的牌
for _ in range(4):
state.hands[0].remove_tile(MahjongTile("", 3))
# 模拟杠上开花自摸一张3万
hand.add_tile(MahjongTile("", 2))
# 设置缺门
state.missing_suits = ""
# 打印手牌,暗牌,明牌,缺门
state.print_game_state(0)
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], state.missing_suits), "测试失败:明杠后杠上开花应该可以胡牌"
print("测试通过:明杠后杠上开花胡牌成功!")
def test_qiang_gang_hu():
"""测试抢杠胡"""
# 玩家 A 的手牌
player_a_hand = Hand()
player_a_hand.add_tile(MahjongTile("", 2))
player_a_hand.add_tile(MahjongTile("", 3))
player_a_hand.add_tile(MahjongTile("", 5))
player_a_hand.add_tile(MahjongTile("", 6))
player_a_hand.add_tile(MahjongTile("", 7))
player_a_hand.add_tile(MahjongTile("", 9))
player_a_hand.add_tile(MahjongTile("", 9))
player_a_hand.add_tile(MahjongTile("", 1))
player_a_hand.add_tile(MahjongTile("", 1))
player_a_hand.add_tile(MahjongTile("", 2))
player_a_hand.add_tile(MahjongTile("", 2))
player_a_hand.add_tile(MahjongTile("", 3))
player_a_hand.add_tile(MahjongTile("", 3))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = player_a_hand # 玩家 A 的手牌
state.missing_suits[0] = "" # 玩家 A 的缺门
# 玩家 B 的明牌和手牌
player_b_hand = Hand()
player_b_hand.add_tile(MahjongTile("", 1))
player_b_hand.add_tile(MahjongTile("", 1))
player_b_hand.add_tile(MahjongTile("", 1))
state.hands[1] = player_b_hand
state.missing_suits[1] = ""
# 玩家 B 尝试杠 1 筒
melds_b = [Meld(MahjongTile("", 1), "")]
state.melds[1] = melds_b
# 玩家 A 抢杠胡
gang_tile = MahjongTile("", 1) # 玩家 B 打出用于杠的牌
player_a_hand.add_tile(gang_tile)
can_qiang_gang_hu = state.can_win(player_a_hand, state.melds[0], state.missing_suits[0])
# 打印状态
state.print_game_state(player_index=0) # 打印玩家 A 的状态
state.print_game_state(player_index=1) # 打印玩家 B 的状态
assert can_qiang_gang_hu, "测试失败:玩家 A 应该可以抢杠胡"
print("测试通过:抢杠胡成功!")
def test_can_win_with_big_pairs():
"""测试大对子胡牌"""
hand = Hand()
# 添加对子
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
# 添加刻子
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 设置缺门为 "条",因为手牌和明牌中没有 "条"
missing_suit = ""
# 打印当前玩家状态
state.print_game_state(player_index=0)
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:大对子应该可以胡牌"
print("测试通过:大对子胡牌成功!")
def test_can_win_with_small_seven_pairs():
"""测试小七对胡牌"""
hand = Hand()
# 添加七对
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
# 设置缺门为 "条",因为手牌和明牌中没有 "条"
missing_suit = ""
# 打印当前玩家状态
state.print_game_state(player_index=0)
# 调用 can_win 方法并断言胡牌
assert state.can_win(state.hands[0], state.melds[0], missing_suit), "测试失败:小七对应该可以胡牌"
print("测试通过:小七对胡牌成功!")
def test_can_win_with_jin_gou_diao():
"""测试金钩吊胡牌"""
hand = Hand()
# 添加仅剩的一张牌
hand.add_tile(MahjongTile("", 3))
# 初始化游戏状态
state = ChengduMahjongState()
state.hands[0] = hand
state.melds[0] = [
Meld(MahjongTile("", 8), ""), # 8筒碰
Meld(MahjongTile("", 7), ""), # 7筒碰
Meld(MahjongTile("", 9), ""), # 9筒杠
Meld(MahjongTile("", 3), "") # 3筒碰
]
# 设置缺门为 "条",因为手牌和明牌中没有 "条"
missing_suit = ""
# 打印当前玩家状态
state.print_game_state(player_index=0)
# 模拟别人打出一张 "筒3",胡牌
winning_tile = MahjongTile("", 3)
state.hands[0].add_tile(winning_tile)
# 调用 can_win 方法并断言胡牌
can_win = state.can_win(state.hands[0], state.melds[0], missing_suit)
assert can_win, f"测试失败:金钩吊未能胡 {winning_tile}"
print("测试通过:金钩吊胡牌成功!")

245
tests/test_fan_type.py Normal file
View File

@@ -0,0 +1,245 @@
from src.engine.hand import Hand
from src.engine.mahjong_tile import MahjongTile
from src.engine.fan_type import is_basic_win,is_cleared,calculate_terminal_fan,is_seven_pairs,is_full_request,is_dragon_seven_pairs
from src.engine.meld import Meld
def test_is_basic_win():
"""
测试平胡(基本胡)的逻辑。
"""
hand = Hand()
# 添加牌到手牌中
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
# 打印当前手牌
print(f"测试手牌: {hand}")
# 调用平胡逻辑函数
result = is_basic_win(hand)
# 使用断言验证
assert result, "测试失败:此手牌应该符合平胡(基本胡)规则"
print("测试通过:平胡(基本胡)逻辑正确")
def test_is_cleared_basic():
"""测试素清(不带杠的清一色)"""
hand = Hand()
# 添加手牌
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
melds = [] # 无杠
assert is_cleared(hand, melds) == 2, "测试失败:素清应为 2 番"
print("测试通过:素清")
def test_is_cleared_with_one_gang():
"""测试极品(带 1 杠的清一色)"""
hand = Hand()
# 添加手牌
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
# 添加明牌1 杠)
melds = [Meld(MahjongTile("", 7), "")]
# 检查是否为极品(带 1 杠的清一色)
assert is_cleared(hand, melds) == 3, "测试失败:极品应为 3 番"
print("测试通过:极品")
def test_calculate_terminal_fan():
"""测试带幺九番型"""
# 示例1基本带幺九
hand = Hand()
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 8))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 9))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
melds = []
assert calculate_terminal_fan(hand, melds) == 3, "测试失败:基本带幺九应为 3 番"
def test_is_seven_pairs():
"""测试七对番型"""
hand = Hand()
# 示例1符合七对
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
assert is_seven_pairs(hand) == 2, "测试失败:符合七对,应为 2 番"
# 示例2不符合七对少一个对子
hand = Hand()
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
assert is_seven_pairs(hand) == 0, "测试失败:不符合七对,应为 0 番"
print("所有七对测试通过!")
def test_is_full_request():
"""测试全求人番型"""
hand = Hand()
# 示例1符合全求人
hand.add_tile(MahjongTile("", 5)) # 玩家手中只剩下 1 张牌
melds = [
Meld(MahjongTile("", 1), ""),
Meld(MahjongTile("", 2), ""),
Meld(MahjongTile("", 3), ""),
Meld(MahjongTile("", 4), ""),
]
winning_tile = MahjongTile("", 5) # 胡牌通过别人打出的牌
assert is_full_request(hand, melds, winning_tile) == 6, "测试失败:符合全求人,应为 6 番"
# 示例2不符合全求人玩家手上有多张牌
hand = Hand()
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
assert is_full_request(hand, melds, winning_tile) == 0, "测试失败:不符合全求人,应为 0 番"
# 示例3不符合全求人没有碰或杠的明牌
hand = Hand()
hand.add_tile(MahjongTile("", 5))
melds = []
assert is_full_request(hand, melds, winning_tile) == 0, "测试失败:不符合全求人,应为 0 番"
print("所有全求人测试通过!")
def test_is_dragon_seven_pairs():
"""测试龙七对番型计算"""
# 示例1符合龙七对
hand = Hand()
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7)) # 四张7筒
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
melds = [] # 没有明牌
fan, root_adjustment = is_dragon_seven_pairs(hand, melds)
assert fan == 12 and root_adjustment == -1, "测试失败:符合龙七对,应为 12 番,并减少 1 根"
# 示例2不符合龙七对只有七对没有四张
hand = Hand()
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
fan, root_adjustment = is_dragon_seven_pairs(hand, melds)
assert fan == 0 and root_adjustment == 0, "测试失败:不符合龙七对,应为 0 番,根数不变"
# 示例3不符合龙七对有明牌
hand = Hand()
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 1))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 2))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 3))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 4))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 5))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 6))
hand.add_tile(MahjongTile("", 7))
hand.add_tile(MahjongTile("", 7))
melds = [Meld(MahjongTile("", 8), "")] # 有明牌
fan, root_adjustment = is_dragon_seven_pairs(hand, melds)
assert fan == 0 and root_adjustment == 0, "测试失败:有明牌,不符合龙七对,应为 0 番,根数不变"
print("所有龙七对测试通过!")

View File

@@ -1,64 +1,87 @@
from src.engine.hand import Hand
from src.engine.mahjong_tile import MahjongTile
def test_hand():
# 创建一个玩家的手牌
def test_add_tile():
"""测试添加牌功能"""
hand = Hand()
tile1 = MahjongTile("", 1)
tile2 = MahjongTile("", 2)
# 添加一些牌到手牌中
hand.add_tile("1条")
hand.add_tile("1条")
hand.add_tile("2条")
hand.add_tile("2条")
hand.add_tile("2条")
hand.add_tile("3条")
hand.add_tile(tile1)
hand.add_tile(tile1)
hand.add_tile(tile2)
print("\n测试添加牌功能,当前手牌:", hand)
# 打印手牌
print("\n当前手牌:", hand)
assert hand.get_tile_count(tile1) == 2, f"测试失败:{tile1} 应该有 2 张"
assert hand.get_tile_count(tile2) == 1, f"测试失败:{tile2} 应该有 1 张"
# 测试获取某张牌的数量
assert hand.get_tile_count("1条") == 2, f"测试失败1条应该有 2 张"
assert hand.get_tile_count("2条") == 3, f"测试失败2条应该有 3 张"
assert hand.get_tile_count("3条") == 1, f"测试失败3条应该有 1 张"
# 测试移除一张牌
hand.remove_tile("1条")
print("移除 1条 后的手牌:", hand)
assert hand.get_tile_count("1") == 1, f"测试失败1条应该有 1 张"
def test_remove_tile():
"""测试移除牌功能"""
hand = Hand()
tile1 = MahjongTile("", 1)
# 确保移除后有足够的牌可以碰
# 添加一张 1条确保可以碰
hand.add_tile("1条")
print("添加 1条 后的手牌:", hand)
hand.add_tile(tile1)
hand.add_tile(tile1)
hand.remove_tile(tile1)
print("\n测试移除牌功能,移除一张 1条 后的手牌:", hand)
# 测试是否可以碰
assert hand.can_peng("1条") == True, f"测试失败1条应该可以碰"
print("可以碰 1条 的牌:", hand.can_peng("1条"))
assert hand.can_peng("3条") == False, f"测试失败3条不可以碰"
print("不可以碰 3条 的牌:", hand.can_peng("3条"))
assert hand.get_tile_count(tile1) == 1, f"测试失败:{tile1} 应该有 1 张"
# 测试是否可以杠
assert hand.can_gang("1条") == False, f"测试失败1条不可以杠"
print("不可以杠 1条 的牌:", hand.can_gang("1条"))
assert hand.can_gang("2条") == False, f"测试失败2条不可以杠"
print("不可以杠 2条 的牌:", hand.can_gang("2"))
def test_can_peng():
"""测试是否可以碰"""
hand = Hand()
tile1 = MahjongTile("", 1)
tile2 = MahjongTile("", 2)
hand.add_tile(tile1)
hand.add_tile(tile1)
print("\n测试碰功能,当前手牌:", hand)
assert hand.can_peng(tile1) == True, f"测试失败:{tile1} 应该可以碰"
assert hand.can_peng(tile2) == False, f"测试失败:{tile2} 不可以碰"
print(f"可以碰 {tile1} 的牌:", hand.can_peng(tile1))
print(f"不可以碰 {tile2} 的牌:", hand.can_peng(tile2))
def test_can_gang():
"""测试是否可以杠"""
hand = Hand()
tile2 = MahjongTile("", 2)
hand.add_tile(tile2)
hand.add_tile(tile2)
hand.add_tile(tile2)
print("\n测试杠功能,当前手牌:", hand)
assert hand.can_gang(tile2) == False, f"测试失败:{tile2} 不可以杠"
# 添加更多牌来形成杠
hand.add_tile("2条")
print("添加牌后手牌:", hand)
hand.add_tile("2条")
print("添加牌后手牌:", hand)
assert hand.can_gang("2条") == False, f"测试失败2条不可以杠" # still not enough for gang
hand.add_tile(tile2)
print("添加一张 2条 后:", hand)
# 添加一张更多的 2条 来形成杠
hand.add_tile("2条")
print("添加一张2条后:", hand)
assert hand.can_gang("2条") == True, f"测试失败2条应该可以杠"
assert hand.can_gang(tile2) == True, f"测试失败:{tile2} 应该可以杠"
def run_all_tests():
"""运行所有测试"""
test_add_tile()
print("测试添加牌功能通过!")
test_remove_tile()
print("测试移除牌功能通过!")
test_can_peng()
print("测试碰功能通过!")
test_can_gang()
print("测试杠功能通过!")
print("\n所有测试通过!")
print("所有测试通过!")
# 运行测试
test_hand()
run_all_tests()