1
pull/1/head
wsy182 2024-11-30 22:07:38 +08:00
parent 4142bd9423
commit ee8bf46701
8 changed files with 219 additions and 29 deletions

View File

@ -1,31 +1,42 @@
from .game_state import ChengduMahjongState
from .utils import get_suit, get_tile_name
import random
from loguru import logger
from .game_state import ChengduMahjongState
class ChengduMahjongEngine:
def __init__(self):
self.state = ChengduMahjongState() # 创建游戏状态
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()
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):
""" 开始游戏 """
# 游戏开始时初始化状态等
self.game_over = False
logger.info("游戏开始!")
if not self.game_started:
self.game_started = True
logger.info("游戏开始!")
else:
logger.warning("游戏已经开始,不能重复启动!")
def check_game_over(self):
""" 检查游戏是否结束 """
@ -33,3 +44,4 @@ class ChengduMahjongEngine:
if len(self.state.deck) == 0:
self.game_over = True
logger.info("游戏结束!")

View File

@ -4,7 +4,7 @@ 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)] # 每个玩家的弃牌列表

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

@ -0,0 +1,37 @@
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_pong(self, tile):
""" 判断是否可以碰即是否有3张相同的牌 """
return self.tile_count[tile] >= 2
def can_gang(self, tile):
""" 判断是否可以杠即是否有4张相同的牌 """
return self.tile_count[tile] >= 3
def __repr__(self):
""" 返回手牌的字符串表示 """
return f"手牌: {self.tiles}, 牌的数量: {dict(self.tile_count)}"

View 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))

View File

@ -1,29 +1,19 @@
def get_suit(tile_index):
"""
根据牌的索引返回花色
索引 0-35索引 36-71索引 72-107
参数:
- tile_index: 牌的索引0-107
返回:
- 花色字符串: """" ""
"""
suits = ["", "", ""]
return suits[tile_index // 36]
if 0 <= tile_index <= 35:
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):
"""
根据牌的索引返回具体的牌花色和数字
参数:
- tile_index: 牌的索引0-107
返回:
- 具体牌的字符串: 例如 "1条""9筒""5万"
根据牌的索引返回牌名例如12筒等
"""
suits = ["", "", ""]
suit = suits[tile_index // 36] # 根据索引获取花色
number = (tile_index % 36) // 4 + 1 # 计算具体数字1-9
return f"{number}{suit}"
suit = get_suit(tile_index)
return f"{tile_index % 36 + 1}{suit}"

50
tests/test_hand.py Normal file
View File

@ -0,0 +1,50 @@
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_pong("1条") == True, f"测试失败1条应该可以碰"
assert hand.can_pong("3条") == False, f"测试失败3条不可以碰"
# 测试是否可以杠
assert hand.can_gang("1条") == False, f"测试失败1条不可以杠"
assert hand.can_gang("2条") == False, f"测试失败2条不可以杠"
# 添加更多牌来形成杠
hand.add_tile("2条")
hand.add_tile("2条")
print("添加牌后手牌:", hand)
assert hand.can_gang("2条") == True, f"测试失败2条应该可以杠"
print("所有测试通过!")

View 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
View 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()