Compare commits
2 Commits
main
...
f8fadb435c
| Author | SHA1 | Date | |
|---|---|---|---|
| f8fadb435c | |||
| b2d9514d2a |
20
README.md
20
README.md
@@ -101,13 +101,13 @@
|
||||
|
||||
3. **带幺九**:
|
||||
|
||||
- **带幺九**:指玩家手上的牌全部是由1和9组成的顺子、刻子或对子。例如,123, 789, 111, 999, 11等。计为3番。<!--存疑-->
|
||||
- **带幺九**:指玩家手上的牌全部是由1和9组成的顺子、刻子或对子。例如,123, 789, 111, 999, 11等。计为3番。
|
||||
|
||||
- **清带幺九**:指玩家手上的牌不仅全部由1和9组成,而且是同一花色(条、筒、万),即清一色的带幺九。计为1番。<!--存疑-->
|
||||
|
||||
4. **七对**:手牌由7个对子组成,计为2番。
|
||||
|
||||
5. **全求人**:所有牌都是通过碰、杠别人打出的牌来完成的,计为6番。
|
||||
5. **全求人**:所有牌都是通过碰、杠、吃别人打出的牌来完成的,计为6番。
|
||||
|
||||
6. **龙七对**:七对中有一对是三张相同的牌,计为12番。
|
||||
|
||||
@@ -206,19 +206,3 @@ TensorBoard 通常会记录和可视化多种训练指标。你提到的这些
|
||||
- **`train/clip_range`**:剪裁范围,反映策略更新的限制。
|
||||
- **`train/clip_fraction`**:被剪裁的比例,反映策略更新的稳定性。
|
||||
- **`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,23 +1,24 @@
|
||||
from .utils import get_suit,get_tile_name
|
||||
from loguru import logger
|
||||
from collections import Counter
|
||||
from .hand import Hand
|
||||
from .mahjong_tile import MahjongTile
|
||||
|
||||
|
||||
class ChengduMahjongState:
|
||||
def __init__(self):
|
||||
# 每个玩家的手牌,使用108个索引表示
|
||||
self.hands = [[0] * 108 for _ in range(4)] # 每个玩家108张牌的计数
|
||||
# 每个玩家的手牌
|
||||
self.hands = [Hand() for _ in range(4)] # 每个玩家的手牌由 Hand 类表示
|
||||
# 每个玩家的打出的牌
|
||||
self.discards = [[] for _ in range(4)] # 每个玩家的弃牌列表
|
||||
# 每个玩家的明牌(碰、杠)
|
||||
self.melds = [[] for _ in range(4)]
|
||||
# 剩余的牌堆
|
||||
self.deck = list(range(108)) # 0-107 表示108张牌
|
||||
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 = 108
|
||||
self.remaining_tiles = len(self.deck)
|
||||
# 胜利玩家列表
|
||||
self.winners = []
|
||||
# 缺门信息
|
||||
@@ -39,21 +40,20 @@ class ChengduMahjongState:
|
||||
raise ValueError("缺门设置无效")
|
||||
self.missing_suits[player] = missing_suit
|
||||
|
||||
def can_win(self, hand):
|
||||
def can_win(self, hand: Hand):
|
||||
"""
|
||||
判断是否满足胡牌条件:四组(顺子或刻子)+ 一对将。
|
||||
"""
|
||||
from collections import Counter
|
||||
|
||||
def is_valid_group(tiles):
|
||||
"""
|
||||
判断是否为合法的顺子或刻子。
|
||||
"""
|
||||
if len(tiles) != 3:
|
||||
return False
|
||||
tiles.sort() # 确保顺子检查按顺序排列
|
||||
tiles.sort(key=lambda t: (t.suit, t.value))
|
||||
return (tiles[0] == tiles[1] == tiles[2]) or \
|
||||
(tiles[0] + 1 == tiles[1] and tiles[1] + 1 == tiles[2])
|
||||
(tiles[0].value + 1 == tiles[1].value and tiles[1].value + 1 == tiles[2].value and
|
||||
tiles[0].suit == tiles[1].suit == tiles[2].suit)
|
||||
|
||||
def try_win(remaining_tiles, depth=0):
|
||||
"""
|
||||
@@ -69,21 +69,22 @@ class ChengduMahjongState:
|
||||
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]
|
||||
# 统计每张牌的数量
|
||||
tile_counter = Counter(hand.tiles)
|
||||
pairs = [tile for tile, count in tile_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):
|
||||
# 移除一对将牌
|
||||
temp_tiles = hand.tiles[:]
|
||||
temp_tiles.remove(pair)
|
||||
temp_tiles.remove(pair)
|
||||
|
||||
# 检查剩余牌是否能分组
|
||||
if try_win(temp_tiles):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -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)}"
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user