Compare commits
10 Commits
fd6006b186
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e85e7d9096 | |||
| 798d1af835 | |||
| 0d723495ce | |||
| 4a9f45b2df | |||
| 96353480be | |||
| 7f06b5648e | |||
| 0dd368cc33 | |||
| e58e890ccb | |||
| ee8bf46701 | |||
| 4142bd9423 |
22
README.md
22
README.md
@@ -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 完整实现的四川麻将游戏的源代码
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import os
|
# configs/log_config.py
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
import os
|
||||||
|
|
||||||
# 确保 ../logs 目录存在,如果不存在则创建
|
def setup_logging():
|
||||||
os.makedirs("../logs", exist_ok=True)
|
# 确保日志目录存在
|
||||||
|
log_dir = "../logs"
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
# 配置日志,记录到 ../logs 目录下
|
# 清除所有现有日志处理器,防止重复配置
|
||||||
logger.add("../logs/chengdu_mj_engine.log", rotation="10 MB", level="DEBUG", format="{time} {level} {message}")
|
logger.remove()
|
||||||
|
|
||||||
|
# 配置日志,记录到 ../logs 目录下
|
||||||
|
logger.add(os.path.join(log_dir, "chengdu_mj_engine.log"), rotation="10 MB", level="DEBUG", format="{time} {level} {message}")
|
||||||
|
|||||||
Binary file not shown.
@@ -2,6 +2,7 @@ import gym
|
|||||||
from stable_baselines3 import PPO
|
from stable_baselines3 import PPO
|
||||||
from src.environment.chengdu_majiang_env import MahjongEnv
|
from src.environment.chengdu_majiang_env import MahjongEnv
|
||||||
import torch
|
import torch
|
||||||
|
from configs.log_config import setup_logging
|
||||||
|
|
||||||
def train_model():
|
def train_model():
|
||||||
# 创建 MahjongEnv 环境实例
|
# 创建 MahjongEnv 环境实例
|
||||||
@@ -29,4 +30,6 @@ def train_model():
|
|||||||
env.render() # 打印环境状态
|
env.render() # 打印环境状态
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# 调用配置函数来设置日志
|
||||||
|
setup_logging()
|
||||||
train_model()
|
train_model()
|
||||||
|
|||||||
@@ -95,3 +95,30 @@ def check_blood_battle(self):
|
|||||||
if len(self.state.winners) >= 3 or self.state.remaining_tiles == 0:
|
if len(self.state.winners) >= 3 or self.state.remaining_tiles == 0:
|
||||||
logger.info(f"游戏结束,赢家列表: {self.state.winners}")
|
logger.info(f"游戏结束,赢家列表: {self.state.winners}")
|
||||||
self.game_over = True
|
self.game_over = True
|
||||||
|
|
||||||
|
|
||||||
|
def set_missing_suit(player, missing_suit, game_state):
|
||||||
|
"""
|
||||||
|
玩家设置缺门的动作。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- player: 玩家索引(0-3)。
|
||||||
|
- missing_suit: 玩家选择的缺门("条"、"筒" 或 "万")。
|
||||||
|
- game_state: 当前的游戏状态(`ChengduMahjongState` 实例)。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 如果缺门设置无效。
|
||||||
|
"""
|
||||||
|
valid_suits = ["条", "筒", "万"]
|
||||||
|
if missing_suit not in valid_suits:
|
||||||
|
logger.error(f"玩家 {player} 尝试设置无效的缺门: {missing_suit}")
|
||||||
|
raise ValueError("缺门设置无效")
|
||||||
|
|
||||||
|
if game_state.missing_suits[player] is not None:
|
||||||
|
logger.error(f"玩家 {player} 已经设置了缺门,不能重复设置")
|
||||||
|
raise ValueError("缺门已经设置,不能重复设置")
|
||||||
|
|
||||||
|
game_state.missing_suits[player] = missing_suit
|
||||||
|
logger.info(f"玩家 {player} 设置缺门为: {missing_suit}")
|
||||||
|
|
||||||
|
return game_state.missing_suits[player]
|
||||||
@@ -1,8 +1,47 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from .game_state import ChengduMahjongState
|
from .game_state import ChengduMahjongState
|
||||||
|
|
||||||
|
|
||||||
class ChengduMahjongEngine:
|
class ChengduMahjongEngine:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.state = ChengduMahjongState()
|
self.state = ChengduMahjongState() # 创建游戏状态
|
||||||
self.game_over = False
|
self.game_over = False
|
||||||
|
self.game_started = False # 游戏是否已开始
|
||||||
|
self.deal_tiles() # 发牌
|
||||||
|
|
||||||
|
def deal_tiles(self):
|
||||||
|
""" 发牌,每个玩家发13张牌,并设置缺门 """
|
||||||
|
logger.info("发牌中...")
|
||||||
|
|
||||||
|
# 洗牌(随机打乱牌堆)
|
||||||
|
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 # 增加玩家手牌的计数
|
||||||
|
|
||||||
|
# 设置缺门:每个玩家定缺(这里假设我们让每个玩家的缺门都为“条”)
|
||||||
|
for player in range(4):
|
||||||
|
missing_suit = "条" # 这里可以通过其他方式设置缺门,比如随机选择
|
||||||
|
self.state.set_missing_suit(player, missing_suit)
|
||||||
|
|
||||||
|
def start_game(self):
|
||||||
|
""" 开始游戏 """
|
||||||
|
if not self.game_started:
|
||||||
|
self.game_started = True
|
||||||
|
logger.info("游戏开始!")
|
||||||
|
else:
|
||||||
|
logger.warning("游戏已经开始,不能重复启动!")
|
||||||
|
|
||||||
|
def check_game_over(self):
|
||||||
|
""" 检查游戏是否结束 """
|
||||||
|
# 你可以根据游戏规则检查是否有玩家胡牌或其他结束条件
|
||||||
|
if len(self.state.deck) == 0:
|
||||||
|
self.game_over = True
|
||||||
|
logger.info("游戏结束!")
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from loguru import logger
|
|||||||
|
|
||||||
class ChengduMahjongState:
|
class ChengduMahjongState:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 每个玩家的手牌
|
# 每个玩家的手牌,使用108个索引表示
|
||||||
self.hands = [[0] * 108 for _ in range(4)] # 每个玩家108张牌的计数
|
self.hands = [[0] * 108 for _ in range(4)] # 每个玩家108张牌的计数
|
||||||
# 每个玩家的打出的牌
|
# 每个玩家的打出的牌
|
||||||
self.discards = [[] for _ in range(4)] # 每个玩家的弃牌列表
|
self.discards = [[] for _ in range(4)] # 每个玩家的弃牌列表
|
||||||
@@ -36,11 +36,8 @@ class ChengduMahjongState:
|
|||||||
"""
|
"""
|
||||||
valid_suits = ["条", "筒", "万"]
|
valid_suits = ["条", "筒", "万"]
|
||||||
if missing_suit not in valid_suits:
|
if missing_suit not in valid_suits:
|
||||||
logger.error(f"玩家 {player} 尝试设置无效的缺门: {missing_suit}")
|
|
||||||
raise ValueError("缺门设置无效")
|
raise ValueError("缺门设置无效")
|
||||||
self.missing_suits[player] = missing_suit
|
self.missing_suits[player] = missing_suit
|
||||||
logger.info(f"玩家 {player} 设置缺门为: {missing_suit}")
|
|
||||||
return self.missing_suits[player]
|
|
||||||
|
|
||||||
def can_win(self, hand):
|
def can_win(self, hand):
|
||||||
"""
|
"""
|
||||||
|
|||||||
39
src/engine/hand.py
Normal file
39
src/engine/hand.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
class Hand:
|
||||||
|
def __init__(self):
|
||||||
|
# 存储所有的牌
|
||||||
|
self.tiles = []
|
||||||
|
# 存储每种牌的数量,默认值为 0
|
||||||
|
self.tile_count = defaultdict(int)
|
||||||
|
|
||||||
|
def add_tile(self, tile):
|
||||||
|
""" 向手牌中添加一张牌 """
|
||||||
|
self.tiles.append(tile) # 将牌添加到手牌中
|
||||||
|
self.tile_count[tile] += 1 # 增加牌的数量
|
||||||
|
|
||||||
|
def remove_tile(self, tile):
|
||||||
|
""" 从手牌中移除一张牌 """
|
||||||
|
if self.tile_count[tile] > 0:
|
||||||
|
self.tiles.remove(tile)
|
||||||
|
self.tile_count[tile] -= 1
|
||||||
|
else:
|
||||||
|
raise ValueError(f"手牌中没有该牌: {tile}")
|
||||||
|
|
||||||
|
def get_tile_count(self, tile):
|
||||||
|
""" 获取手牌中某张牌的数量 """
|
||||||
|
return self.tile_count[tile]
|
||||||
|
|
||||||
|
def can_peng(self, tile):
|
||||||
|
""" 判断是否可以碰(即是否已经有2张相同的牌,摸一张牌后可以碰) """
|
||||||
|
return self.tile_count[tile] == 2 # 摸一张牌后总数为 3 张,才可以碰
|
||||||
|
|
||||||
|
def can_gang(self, tile):
|
||||||
|
""" 判断是否可以杠(即是否已经有3张相同的牌,摸一张牌后可以杠) """
|
||||||
|
return self.tile_count[tile] == 3 # 摸一张牌后总数为 4 张,才可以杠
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
""" 返回手牌的字符串表示 """
|
||||||
|
return f"手牌: {self.tiles}, 牌的数量: {dict(self.tile_count)}"
|
||||||
20
src/engine/mahjong_tile.py
Normal file
20
src/engine/mahjong_tile.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class MahjongTile:
|
||||||
|
SUITS = ['条', '筒', '万']
|
||||||
|
|
||||||
|
def __init__(self, suit, value):
|
||||||
|
if suit not in self.SUITS or not (1 <= value <= 9):
|
||||||
|
raise ValueError("Invalid tile")
|
||||||
|
self.suit = suit
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.value}{self.suit}"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.suit == other.suit and self.value == other.value
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.suit, self.value))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,29 +1,19 @@
|
|||||||
def get_suit(tile_index):
|
def get_suit(tile_index):
|
||||||
"""
|
"""
|
||||||
根据牌的索引返回花色。
|
根据牌的索引返回花色。
|
||||||
条:索引 0-35,筒:索引 36-71,万:索引 72-107
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- tile_index: 牌的索引(0-107)。
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- 花色字符串: "条"、"筒" 或 "万"。
|
|
||||||
"""
|
"""
|
||||||
suits = ["条", "筒", "万"]
|
if 0 <= tile_index <= 35:
|
||||||
return suits[tile_index // 36]
|
return "条"
|
||||||
|
elif 36 <= tile_index <= 71:
|
||||||
|
return "筒"
|
||||||
|
elif 72 <= tile_index <= 107:
|
||||||
|
return "万"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"无效的牌索引: {tile_index}")
|
||||||
|
|
||||||
def get_tile_name(tile_index):
|
def get_tile_name(tile_index):
|
||||||
"""
|
"""
|
||||||
根据牌的索引返回具体的牌(花色和数字)。
|
根据牌的索引返回牌名(例如:1条,2筒等)。
|
||||||
|
|
||||||
参数:
|
|
||||||
- tile_index: 牌的索引(0-107)。
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- 具体牌的字符串: 例如 "1条"、"9筒"、"5万"。
|
|
||||||
"""
|
"""
|
||||||
suits = ["条", "筒", "万"]
|
suit = get_suit(tile_index)
|
||||||
suit = suits[tile_index // 36] # 根据索引获取花色
|
return f"{tile_index % 36 + 1}{suit}"
|
||||||
number = (tile_index % 36) // 4 + 1 # 计算具体数字(1-9)
|
|
||||||
return f"{number}{suit}"
|
|
||||||
|
|||||||
64
tests/test_hand.py
Normal file
64
tests/test_hand.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from src.engine.hand import Hand
|
||||||
|
|
||||||
|
|
||||||
|
def test_hand():
|
||||||
|
# 创建一个玩家的手牌
|
||||||
|
hand = Hand()
|
||||||
|
|
||||||
|
# 添加一些牌到手牌中
|
||||||
|
hand.add_tile("1条")
|
||||||
|
hand.add_tile("1条")
|
||||||
|
hand.add_tile("2条")
|
||||||
|
hand.add_tile("2条")
|
||||||
|
hand.add_tile("2条")
|
||||||
|
hand.add_tile("3条")
|
||||||
|
|
||||||
|
# 打印手牌
|
||||||
|
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 张"
|
||||||
|
|
||||||
|
# 测试移除一张牌
|
||||||
|
hand.remove_tile("1条")
|
||||||
|
print("移除 1条 后的手牌:", hand)
|
||||||
|
assert hand.get_tile_count("1条") == 1, f"测试失败:1条应该有 1 张"
|
||||||
|
|
||||||
|
# 确保移除后有足够的牌可以碰
|
||||||
|
# 添加一张 1条,确保可以碰
|
||||||
|
hand.add_tile("1条")
|
||||||
|
print("添加 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.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条"))
|
||||||
|
|
||||||
|
# 添加更多牌来形成杠
|
||||||
|
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
|
||||||
|
|
||||||
|
# 添加一张更多的 2条 来形成杠
|
||||||
|
hand.add_tile("2条")
|
||||||
|
print("添加一张2条后:", hand)
|
||||||
|
assert hand.can_gang("2条") == True, f"测试失败:2条应该可以杠"
|
||||||
|
|
||||||
|
print("所有测试通过!")
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
test_hand()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
45
tests/test_mahjong_tile.py
Normal file
45
tests/test_mahjong_tile.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from src.engine.mahjong_tile import MahjongTile
|
||||||
|
|
||||||
|
def test_mahjong_tile():
|
||||||
|
# 测试合法的牌
|
||||||
|
tile1 = MahjongTile("条", 5)
|
||||||
|
assert tile1.suit == "条", f"测试失败:预期花色是 '条',但实际是 {tile1.suit}"
|
||||||
|
assert tile1.value == 5, f"测试失败:预期面值是 5,但实际是 {tile1.value}"
|
||||||
|
assert repr(tile1) == "5条", f"测试失败:预期牌名是 '5条',但实际是 {repr(tile1)}"
|
||||||
|
|
||||||
|
tile2 = MahjongTile("筒", 3)
|
||||||
|
assert tile2.suit == "筒", f"测试失败:预期花色是 '筒',但实际是 {tile2.suit}"
|
||||||
|
assert tile2.value == 3, f"测试失败:预期面值是 3,但实际是 {tile2.value}"
|
||||||
|
assert repr(tile2) == "3筒", f"测试失败:预期牌名是 '3筒',但实际是 {repr(tile2)}"
|
||||||
|
|
||||||
|
tile3 = MahjongTile("万", 9)
|
||||||
|
assert tile3.suit == "万", f"测试失败:预期花色是 '万',但实际是 {tile3.suit}"
|
||||||
|
assert tile3.value == 9, f"测试失败:预期面值是 9,但实际是 {tile3.value}"
|
||||||
|
assert repr(tile3) == "9万", f"测试失败:预期牌名是 '9万',但实际是 {repr(tile3)}"
|
||||||
|
|
||||||
|
# 测试非法的牌
|
||||||
|
try:
|
||||||
|
MahjongTile("条", 10) # 面值超出范围
|
||||||
|
assert False, "测试失败:面值为 10 的牌应该抛出异常"
|
||||||
|
except ValueError:
|
||||||
|
pass # 正确抛出异常
|
||||||
|
|
||||||
|
try:
|
||||||
|
MahjongTile("花", 5) # 花色无效
|
||||||
|
assert False, "测试失败:花色为 '花' 的牌应该抛出异常"
|
||||||
|
except ValueError:
|
||||||
|
pass # 正确抛出异常
|
||||||
|
|
||||||
|
# 测试相等判断
|
||||||
|
tile4 = MahjongTile("条", 5)
|
||||||
|
assert tile1 == tile4, f"测试失败:预期 {tile1} 和 {tile4} 相等"
|
||||||
|
tile5 = MahjongTile("筒", 5)
|
||||||
|
assert tile1 != tile5, f"测试失败:预期 {tile1} 和 {tile5} 不相等"
|
||||||
|
|
||||||
|
# 测试哈希
|
||||||
|
tile_set = {tile1, tile4, tile2}
|
||||||
|
assert len(tile_set) == 2, f"测试失败:集合中应该有 2 张牌,而实际有 {len(tile_set)} 张"
|
||||||
|
|
||||||
|
print("所有测试通过!")
|
||||||
|
|
||||||
|
|
||||||
36
tests/test_utils.py
Normal file
36
tests/test_utils.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from src.engine.utils import get_suit,get_tile_name
|
||||||
|
|
||||||
|
def test_get_suit():
|
||||||
|
# 测试条花色(0-35)
|
||||||
|
for i in range(36):
|
||||||
|
assert get_suit(i) == "条", f"测试失败:索引 {i} 应该是 '条'"
|
||||||
|
|
||||||
|
# 测试筒花色(36-71)
|
||||||
|
for i in range(36, 72):
|
||||||
|
assert get_suit(i) == "筒", f"测试失败:索引 {i} 应该是 '筒'"
|
||||||
|
|
||||||
|
# 测试万花色(72-107)
|
||||||
|
for i in range(72, 108):
|
||||||
|
assert get_suit(i) == "万", f"测试失败:索引 {i} 应该是 '万'"
|
||||||
|
|
||||||
|
# 测试无效索引
|
||||||
|
try:
|
||||||
|
get_suit(108)
|
||||||
|
assert False, "测试失败:索引 108 应该抛出 ValueError"
|
||||||
|
except ValueError:
|
||||||
|
pass # 如果抛出 ValueError,测试通过
|
||||||
|
|
||||||
|
print("get_suit 测试通过!")
|
||||||
|
|
||||||
|
def test_get_tile_name():
|
||||||
|
# 测试每个牌的名称是否正确
|
||||||
|
for i in range(108):
|
||||||
|
tile_name = get_tile_name(i)
|
||||||
|
assert tile_name == f"{i % 36 + 1}{get_suit(i)}", \
|
||||||
|
f"测试失败:索引 {i} 应该是 '{i % 36 + 1}{get_suit(i)}',但实际返回 '{tile_name}'"
|
||||||
|
|
||||||
|
print("get_tile_name 测试通过!")
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
test_get_suit()
|
||||||
|
test_get_tile_name()
|
||||||
Reference in New Issue
Block a user