Compare commits
7 Commits
8a966890d5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e85e7d9096 | |||
| 798d1af835 | |||
| 0d723495ce | |||
| 4a9f45b2df | |||
| 96353480be | |||
| 7f06b5648e | |||
| 0dd368cc33 |
24
README.md
24
README.md
@@ -77,7 +77,7 @@
|
|||||||
- **定缺**:游戏开始时,每位玩家需要扣下一张牌作为自己缺的那门,并且不能更改。如果本身就是两门牌,则可以报“天缺”而不扣牌。
|
- **定缺**:游戏开始时,每位玩家需要扣下一张牌作为自己缺的那门,并且不能更改。如果本身就是两门牌,则可以报“天缺”而不扣牌。
|
||||||
- **起牌与打牌**:庄家通过掷骰子决定起牌位置,然后按顺序抓牌。庄家先出牌,之后每家依次摸牌打牌。
|
- **起牌与打牌**:庄家通过掷骰子决定起牌位置,然后按顺序抓牌。庄家先出牌,之后每家依次摸牌打牌。
|
||||||
- **碰、杠**:允许碰牌和杠牌,但不允许吃牌。杠牌分为明杠和暗杠,明杠是其他玩家打出的牌被你碰后又摸到相同的牌;暗杠则是你自己摸到四张相同的牌。
|
- **碰、杠**:允许碰牌和杠牌,但不允许吃牌。杠牌分为明杠和暗杠,明杠是其他玩家打出的牌被你碰后又摸到相同的牌;暗杠则是你自己摸到四张相同的牌。
|
||||||
- **胡牌**:胡牌的基本条件是拥有一个对子加上四个顺子或刻子(三个相同牌)。自摸为三家给分,点炮则由放炮者给分。n*AAA+m*ABC+DD ,mn可以等于0。
|
- **胡牌**:胡牌的基本条件是拥有一个对子加上四个顺子或刻子(三个相同牌)。自摸为三家给分,点炮则由放炮者给分。
|
||||||
- **血战到底**:一家胡牌后,其他未胡牌的玩家继续游戏,直到只剩下最后一位玩家或者黄庄(所有牌都被摸完)为止。
|
- **血战到底**:一家胡牌后,其他未胡牌的玩家继续游戏,直到只剩下最后一位玩家或者黄庄(所有牌都被摸完)为止。
|
||||||
|
|
||||||
### 特殊规则
|
### 特殊规则
|
||||||
@@ -101,13 +101,13 @@
|
|||||||
|
|
||||||
3. **带幺九**:
|
3. **带幺九**:
|
||||||
|
|
||||||
- **带幺九**:指玩家手上的牌全部是由1和9组成的顺子、刻子或对子。例如,123, 789, 111, 999, 11等。计为3番。
|
- **带幺九**:指玩家手上的牌全部是由1和9组成的顺子、刻子或对子。例如,123, 789, 111, 999, 11等。计为3番。<!--存疑-->
|
||||||
|
|
||||||
- **清带幺九**:指玩家手上的牌不仅全部由1和9组成,而且是同一花色(条、筒、万),即清一色的带幺九。计为1番。<!--存疑-->
|
- **清带幺九**:指玩家手上的牌不仅全部由1和9组成,而且是同一花色(条、筒、万),即清一色的带幺九。计为1番。<!--存疑-->
|
||||||
|
|
||||||
4. **七对**:手牌由7个对子组成,计为2番。
|
4. **七对**:手牌由7个对子组成,计为2番。
|
||||||
|
|
||||||
5. **全求人**:所有牌都是通过碰、杠、吃别人打出的牌来完成的,计为6番。
|
5. **全求人**:所有牌都是通过碰、杠别人打出的牌来完成的,计为6番。
|
||||||
|
|
||||||
6. **龙七对**:七对中有一对是三张相同的牌,计为12番。
|
6. **龙七对**:七对中有一对是三张相同的牌,计为12番。
|
||||||
|
|
||||||
@@ -205,4 +205,20 @@ TensorBoard 通常会记录和可视化多种训练指标。你提到的这些
|
|||||||
- **`train/entropy_loss`**:熵损失,反映策略的探索程度。
|
- **`train/entropy_loss`**:熵损失,反映策略的探索程度。
|
||||||
- **`train/clip_range`**:剪裁范围,反映策略更新的限制。
|
- **`train/clip_range`**:剪裁范围,反映策略更新的限制。
|
||||||
- **`train/clip_fraction`**:被剪裁的比例,反映策略更新的稳定性。
|
- **`train/clip_fraction`**:被剪裁的比例,反映策略更新的稳定性。
|
||||||
- **`train/approx_kl`**:近似 KL 散度,反映策略更新的幅度和稳定性。
|
- **`train/approx_kl`**:近似 KL 散度,反映策略更新的幅度和稳定性。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
|
||||||
|
https://github.com/mangenotwork/CLI-Sichuan-Mahjong //golang命令行麻将
|
||||||
|
|
||||||
|
https://github.com/lauyikfung/SichuaMahjongAI //SichuaMahjongAI
|
||||||
|
|
||||||
|
https://github.com/risseraka/node-sichuan-mahjong //nodejs
|
||||||
|
|
||||||
|
https://github.imc.re/latorc/MahjongCopilot //麻将 AI 助手,基于 mjai (Mortal模型) 实现的机器人。
|
||||||
|
|
||||||
|
https://github.com/kennyzhang0819/Sichuan-Mahjong-AI-Testbed // Java 完整实现的四川麻将游戏的源代码
|
||||||
@@ -2,7 +2,7 @@ import random
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .chengdu_mahjong_state import ChengduMahjongState
|
from .game_state import ChengduMahjongState
|
||||||
|
|
||||||
|
|
||||||
class ChengduMahjongEngine:
|
class ChengduMahjongEngine:
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
from collections import Counter
|
|
||||||
from .hand import Hand
|
|
||||||
from .mahjong_tile import MahjongTile
|
|
||||||
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)]
|
|
||||||
# 剩余的牌堆
|
|
||||||
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 can_win(self, hand: Hand, melds: list, missing_suit: str):
|
|
||||||
"""
|
|
||||||
判断玩家是否能胡牌。
|
|
||||||
:param hand: 玩家手牌(Hand 对象)。
|
|
||||||
:param melds: 玩家已明牌的列表(碰、杠)。
|
|
||||||
: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, groups_formed=0):
|
|
||||||
"""
|
|
||||||
尝试将剩余牌分组,必须满足 n * AAA + m * ABC + DD。
|
|
||||||
"""
|
|
||||||
if not remaining_tiles:
|
|
||||||
return groups_formed >= 4 # 至少形成 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:]
|
|
||||||
if try_win(next_tiles, groups_formed + 1):
|
|
||||||
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):
|
|
||||||
return False # 仍有缺门的花色,不能胡牌
|
|
||||||
|
|
||||||
# **第二步:合并暗牌和明牌**
|
|
||||||
all_tiles = hand.tiles[:]
|
|
||||||
for meld in melds:
|
|
||||||
if meld[0] == "碰" or meld[0] == "杠": # 将明牌的 AAA 加入检查
|
|
||||||
all_tiles.extend([meld[1], meld[1], meld[1]])
|
|
||||||
|
|
||||||
# **第三步:寻找对子并分组**
|
|
||||||
# 找到所有对子(至少两张相同的牌)
|
|
||||||
tile_counter = Counter(all_tiles)
|
|
||||||
pairs = [tile for tile, count in tile_counter.items() if count >= 2]
|
|
||||||
|
|
||||||
# 遍历所有对子,尝试用剩余牌分组
|
|
||||||
for pair in pairs:
|
|
||||||
temp_tiles = all_tiles[:]
|
|
||||||
temp_tiles.remove(pair) # 移除一张将牌
|
|
||||||
temp_tiles.remove(pair) # 再移除一张将牌
|
|
||||||
if try_win(temp_tiles): # 检查是否可以分组
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False # 如果没有找到符合条件的组合,则不能胡牌
|
|
||||||
89
src/engine/game_state.py
Normal file
89
src/engine/game_state.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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
|
||||||
@@ -1,24 +1,21 @@
|
|||||||
from src.engine.mahjong_tile import MahjongTile
|
from collections import defaultdict
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
class Hand:
|
class Hand:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 存储所有的 MahjongTile 对象
|
# 存储所有的牌
|
||||||
self.tiles = []
|
self.tiles = []
|
||||||
# 存储每种牌的数量,键为 MahjongTile 对象,值为数量
|
# 存储每种牌的数量,默认值为 0
|
||||||
self.tile_count = defaultdict(int)
|
self.tile_count = defaultdict(int)
|
||||||
|
|
||||||
def add_tile(self, tile):
|
def add_tile(self, tile):
|
||||||
""" 向手牌中添加一张牌 """
|
""" 向手牌中添加一张牌 """
|
||||||
if not isinstance(tile, MahjongTile):
|
|
||||||
raise ValueError("必须添加 MahjongTile 类型的牌")
|
|
||||||
self.tiles.append(tile) # 将牌添加到手牌中
|
self.tiles.append(tile) # 将牌添加到手牌中
|
||||||
self.tile_count[tile] += 1 # 增加牌的数量
|
self.tile_count[tile] += 1 # 增加牌的数量
|
||||||
|
|
||||||
def remove_tile(self, tile):
|
def remove_tile(self, tile):
|
||||||
""" 从手牌中移除一张牌 """
|
""" 从手牌中移除一张牌 """
|
||||||
if not isinstance(tile, MahjongTile):
|
|
||||||
raise ValueError("必须移除 MahjongTile 类型的牌")
|
|
||||||
if self.tile_count[tile] > 0:
|
if self.tile_count[tile] > 0:
|
||||||
self.tiles.remove(tile)
|
self.tiles.remove(tile)
|
||||||
self.tile_count[tile] -= 1
|
self.tile_count[tile] -= 1
|
||||||
@@ -27,23 +24,16 @@ class Hand:
|
|||||||
|
|
||||||
def get_tile_count(self, tile):
|
def get_tile_count(self, tile):
|
||||||
""" 获取手牌中某张牌的数量 """
|
""" 获取手牌中某张牌的数量 """
|
||||||
if not isinstance(tile, MahjongTile):
|
|
||||||
raise ValueError("必须是 MahjongTile 类型的牌")
|
|
||||||
return self.tile_count[tile]
|
return self.tile_count[tile]
|
||||||
|
|
||||||
def can_peng(self, tile):
|
def can_peng(self, tile):
|
||||||
""" 判断是否可以碰(即是否已经有2张相同的牌,摸一张牌后可以碰) """
|
""" 判断是否可以碰(即是否已经有2张相同的牌,摸一张牌后可以碰) """
|
||||||
if not isinstance(tile, MahjongTile):
|
|
||||||
raise ValueError("必须是 MahjongTile 类型的牌")
|
|
||||||
return self.tile_count[tile] == 2 # 摸一张牌后总数为 3 张,才可以碰
|
return self.tile_count[tile] == 2 # 摸一张牌后总数为 3 张,才可以碰
|
||||||
|
|
||||||
def can_gang(self, tile):
|
def can_gang(self, tile):
|
||||||
""" 判断是否可以杠(即是否已经有3张相同的牌,摸一张牌后可以杠) """
|
""" 判断是否可以杠(即是否已经有3张相同的牌,摸一张牌后可以杠) """
|
||||||
if not isinstance(tile, MahjongTile):
|
return self.tile_count[tile] == 3 # 摸一张牌后总数为 4 张,才可以杠
|
||||||
raise ValueError("必须是 MahjongTile 类型的牌")
|
|
||||||
return self.tile_count[tile] == 4 # 摸一张牌后总数为 4 张,才可以杠
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
""" 返回手牌的字符串表示 """
|
""" 返回手牌的字符串表示 """
|
||||||
tiles_str = ", ".join(str(tile) for tile in self.tiles)
|
return f"手牌: {self.tiles}, 牌的数量: {dict(self.tile_count)}"
|
||||||
return f"手牌: [{tiles_str}], 牌的数量: {dict(self.tile_count)}"
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
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 == '杠'
|
|
||||||
38
test.py
38
test.py
@@ -1,35 +1,3 @@
|
|||||||
from src.engine.chengdu_mahjong_state import ChengduMahjongState
|
import torch
|
||||||
from src.engine.hand import Hand
|
print(torch.cuda.is_available()) # 如果返回True,说明可以使用GPU
|
||||||
|
print(torch.__version__)
|
||||||
from src.engine.mahjong_tile import MahjongTile
|
|
||||||
|
|
||||||
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 = [
|
|
||||||
("杠", MahjongTile("筒", 9)), # 表示明杠了4张9筒
|
|
||||||
]
|
|
||||||
state.melds[0] = melds_list
|
|
||||||
|
|
||||||
# 设置缺门为 "万"
|
|
||||||
missing_suit = "万"
|
|
||||||
print(f"\n当前手牌: {state.hands[0]}, 明牌: {state.melds[0]}")
|
|
||||||
state.can_win(state.hands[0], state.melds[0], missing_suit)
|
|
||||||
@@ -27,7 +27,7 @@ def test_discard_tile():
|
|||||||
|
|
||||||
|
|
||||||
def test_set_missing_suit():
|
def test_set_missing_suit():
|
||||||
from src.engine.chengdu_mahjong_state import ChengduMahjongState
|
from src.engine.game_state import ChengduMahjongState
|
||||||
|
|
||||||
state = ChengduMahjongState()
|
state = ChengduMahjongState()
|
||||||
player = 0
|
player = 0
|
||||||
@@ -41,7 +41,7 @@ def test_set_missing_suit():
|
|||||||
|
|
||||||
|
|
||||||
def test_can_win():
|
def test_can_win():
|
||||||
from src.engine.chengdu_mahjong_state import ChengduMahjongState
|
from src.engine.game_state import ChengduMahjongState
|
||||||
|
|
||||||
state = ChengduMahjongState()
|
state = ChengduMahjongState()
|
||||||
hand = [0] * 108
|
hand = [0] * 108
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
from src.engine.chengdu_mahjong_state import ChengduMahjongState
|
|
||||||
from src.engine.hand import Hand
|
|
||||||
from src.engine.mahjong_tile import MahjongTile
|
|
||||||
|
|
||||||
|
|
||||||
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 = [
|
|
||||||
("杠", MahjongTile("筒", 9))
|
|
||||||
]
|
|
||||||
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) == True, "测试失败:带杠的清一色应该可以胡牌"
|
|
||||||
@@ -1,87 +1,64 @@
|
|||||||
from src.engine.hand import Hand
|
from src.engine.hand import Hand
|
||||||
from src.engine.mahjong_tile import MahjongTile
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_tile():
|
def test_hand():
|
||||||
"""测试添加牌功能"""
|
# 创建一个玩家的手牌
|
||||||
hand = Hand()
|
hand = Hand()
|
||||||
tile1 = MahjongTile("条", 1)
|
|
||||||
tile2 = MahjongTile("条", 2)
|
|
||||||
|
|
||||||
hand.add_tile(tile1)
|
# 添加一些牌到手牌中
|
||||||
hand.add_tile(tile1)
|
hand.add_tile("1条")
|
||||||
hand.add_tile(tile2)
|
hand.add_tile("1条")
|
||||||
print("\n测试添加牌功能,当前手牌:", hand)
|
hand.add_tile("2条")
|
||||||
|
hand.add_tile("2条")
|
||||||
|
hand.add_tile("2条")
|
||||||
|
hand.add_tile("3条")
|
||||||
|
|
||||||
assert hand.get_tile_count(tile1) == 2, f"测试失败:{tile1} 应该有 2 张"
|
# 打印手牌
|
||||||
assert hand.get_tile_count(tile2) == 1, f"测试失败:{tile2} 应该有 1 张"
|
print("\n当前手牌:", hand)
|
||||||
|
|
||||||
|
# 测试获取某张牌的数量
|
||||||
|
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 张"
|
||||||
|
|
||||||
def test_remove_tile():
|
# 测试移除一张牌
|
||||||
"""测试移除牌功能"""
|
hand.remove_tile("1条")
|
||||||
hand = Hand()
|
print("移除 1条 后的手牌:", hand)
|
||||||
tile1 = MahjongTile("条", 1)
|
assert hand.get_tile_count("1条") == 1, f"测试失败:1条应该有 1 张"
|
||||||
|
|
||||||
hand.add_tile(tile1)
|
# 确保移除后有足够的牌可以碰
|
||||||
hand.add_tile(tile1)
|
# 添加一张 1条,确保可以碰
|
||||||
hand.remove_tile(tile1)
|
hand.add_tile("1条")
|
||||||
print("\n测试移除牌功能,移除一张 1条 后的手牌:", hand)
|
print("添加 1条 后的手牌:", hand)
|
||||||
|
|
||||||
assert hand.get_tile_count(tile1) == 1, f"测试失败:{tile1} 应该有 1 张"
|
# 测试是否可以碰
|
||||||
|
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条"))
|
||||||
|
|
||||||
|
# 测试是否可以杠
|
||||||
def test_can_peng():
|
assert hand.can_gang("1条") == False, f"测试失败:1条不可以杠"
|
||||||
"""测试是否可以碰"""
|
print("不可以杠 1条 的牌:", hand.can_gang("1条"))
|
||||||
hand = Hand()
|
assert hand.can_gang("2条") == False, f"测试失败:2条不可以杠"
|
||||||
tile1 = MahjongTile("条", 1)
|
print("不可以杠 2条 的牌:", hand.can_gang("2条"))
|
||||||
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(tile2)
|
hand.add_tile("2条")
|
||||||
print("再添加一张 2条 后:", hand)
|
print("添加牌后手牌:", hand)
|
||||||
|
hand.add_tile("2条")
|
||||||
|
print("添加牌后手牌:", hand)
|
||||||
|
assert hand.can_gang("2条") == False, f"测试失败:2条不可以杠" # still not enough for gang
|
||||||
|
|
||||||
assert hand.can_gang(tile2) == True, f"测试失败:{tile2} 应该可以杠"
|
# 添加一张更多的 2条 来形成杠
|
||||||
|
hand.add_tile("2条")
|
||||||
|
print("添加一张2条后:", hand)
|
||||||
def run_all_tests():
|
assert hand.can_gang("2条") == True, f"测试失败:2条应该可以杠"
|
||||||
"""运行所有测试"""
|
|
||||||
test_add_tile()
|
|
||||||
print("测试添加牌功能通过!")
|
|
||||||
|
|
||||||
test_remove_tile()
|
|
||||||
print("测试移除牌功能通过!")
|
|
||||||
|
|
||||||
test_can_peng()
|
|
||||||
print("测试碰功能通过!")
|
|
||||||
|
|
||||||
test_can_gang()
|
|
||||||
print("测试杠功能通过!")
|
|
||||||
|
|
||||||
print("\n所有测试通过!")
|
|
||||||
|
|
||||||
|
print("所有测试通过!")
|
||||||
|
|
||||||
# 运行测试
|
# 运行测试
|
||||||
run_all_tests()
|
test_hand()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user