339 lines
9.1 KiB
Python
339 lines
9.1 KiB
Python
"""
|
|
斗地主游戏逻辑
|
|
牌型识别、牌组管理、规则验证
|
|
"""
|
|
import random
|
|
from typing import List, Dict, Optional, Any
|
|
from dataclasses import dataclass, asdict
|
|
from enum import Enum
|
|
|
|
|
|
class CardType(Enum):
|
|
"""牌型枚举"""
|
|
SINGLE = "single" # 单张
|
|
PAIR = "pair" # 对子
|
|
TRIPLE = "triple" # 三张
|
|
TRIPLE_WITH_SINGLE = "triple_with_single" # 三带一
|
|
TRIPLE_WITH_PAIR = "triple_with_pair" # 三带二
|
|
STRAIGHT = "straight" # 顺子
|
|
DOUBLE_STRAIGHT = "double_straight" # 连对
|
|
TRIPLE_STRAIGHT = "triple_straight" # 飞机
|
|
QUAD_WITH_TWO = "quad_with_two" # 四带二
|
|
BOMB = "bomb" # 炸弹
|
|
JOKER_BOMB = "joker_bomb" # 王炸
|
|
PASS = "pass" # 不出
|
|
|
|
|
|
@dataclass
|
|
class Card:
|
|
"""扑克牌"""
|
|
id: int
|
|
rank: str
|
|
suit: str
|
|
value: int
|
|
is_joker: bool = False
|
|
|
|
def to_dict(self) -> Dict:
|
|
return asdict(self)
|
|
|
|
def __repr__(self):
|
|
return f"{self.suit}{self.rank}"
|
|
|
|
|
|
class Deck:
|
|
"""牌组管理"""
|
|
|
|
SUITS = ['♠', '♥', '♣', '♦']
|
|
RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
|
|
|
|
@classmethod
|
|
def create_full_deck(cls) -> List[Card]:
|
|
"""创建完整牌组(54张)"""
|
|
deck = []
|
|
card_id = 0
|
|
|
|
# 52张普通牌
|
|
for rank in cls.RANKS:
|
|
for suit in cls.SUITS:
|
|
value = cls.RANKS.index(rank) + 3 # 3=3, ..., A=14, 2=15
|
|
deck.append(Card(
|
|
id=card_id,
|
|
rank=rank,
|
|
suit=suit,
|
|
value=value,
|
|
is_joker=False
|
|
))
|
|
card_id += 1
|
|
|
|
# 小王
|
|
deck.append(Card(
|
|
id=card_id,
|
|
rank='小王',
|
|
suit='🃏',
|
|
value=16,
|
|
is_joker=True
|
|
))
|
|
card_id += 1
|
|
|
|
# 大王
|
|
deck.append(Card(
|
|
id=card_id,
|
|
rank='大王',
|
|
suit='🃏',
|
|
value=17,
|
|
is_joker=True
|
|
))
|
|
|
|
return deck
|
|
|
|
@classmethod
|
|
def shuffle(cls, deck: List[Card]) -> List[Card]:
|
|
"""洗牌"""
|
|
shuffled = deck.copy()
|
|
random.shuffle(shuffled)
|
|
return shuffled
|
|
|
|
@classmethod
|
|
def sort_cards(cls, cards: List[Card]) -> List[Card]:
|
|
"""按牌值排序"""
|
|
return sorted(cards, key=lambda c: c.value)
|
|
|
|
|
|
def identify_card_type(cards: List[Card]) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
识别牌型
|
|
返回: {type: str, value: int, length: int, name: str, cards: List[Card]}
|
|
注意: type 返回字符串而非 CardType 枚举,便于 JSON 序列化
|
|
"""
|
|
if not cards or len(cards) == 0:
|
|
return None
|
|
|
|
n = len(cards)
|
|
sorted_cards = Deck.sort_cards(cards)
|
|
values = [c.value for c in sorted_cards]
|
|
|
|
# 统计每个牌值的数量
|
|
value_count = {}
|
|
for v in values:
|
|
value_count[v] = value_count.get(v, 0) + 1
|
|
|
|
counts = sorted(value_count.values(), reverse=True)
|
|
unique_values = sorted(value_count.keys())
|
|
|
|
# 王炸
|
|
if n == 2 and all(c.is_joker for c in cards):
|
|
return {
|
|
'type': 'joker_bomb',
|
|
'value': 100,
|
|
'length': 2,
|
|
'name': '王炸',
|
|
'cards': cards
|
|
}
|
|
|
|
# 单张
|
|
if n == 1:
|
|
return {
|
|
'type': 'single',
|
|
'value': values[0],
|
|
'length': 1,
|
|
'name': f'单张{sorted_cards[0].rank}',
|
|
'cards': cards
|
|
}
|
|
|
|
# 对子
|
|
if n == 2 and counts == [2]:
|
|
return {
|
|
'type': 'pair',
|
|
'value': values[0],
|
|
'length': 2,
|
|
'name': f'对子{sorted_cards[0].rank}',
|
|
'cards': cards
|
|
}
|
|
|
|
# 三张
|
|
if n == 3 and counts == [3]:
|
|
return {
|
|
'type': 'triple',
|
|
'value': values[0],
|
|
'length': 3,
|
|
'name': f'三张{sorted_cards[0].rank}',
|
|
'cards': cards
|
|
}
|
|
|
|
# 三带一
|
|
if n == 4 and counts == [3, 1]:
|
|
triple_value = [v for v, c in value_count.items() if c == 3][0]
|
|
return {
|
|
'type': 'triple_with_single',
|
|
'value': triple_value,
|
|
'length': 4,
|
|
'name': f'三带一',
|
|
'cards': cards
|
|
}
|
|
|
|
# 三带二
|
|
if n == 5 and counts == [3, 2]:
|
|
triple_value = [v for v, c in value_count.items() if c == 3][0]
|
|
return {
|
|
'type': 'triple_with_pair',
|
|
'value': triple_value,
|
|
'length': 5,
|
|
'name': f'三带二',
|
|
'cards': cards
|
|
}
|
|
|
|
# 炸弹
|
|
if n == 4 and counts == [4]:
|
|
return {
|
|
'type': 'bomb',
|
|
'value': values[0],
|
|
'length': 4,
|
|
'name': f'炸弹{sorted_cards[0].rank}',
|
|
'cards': cards
|
|
}
|
|
|
|
# 顺子 (>=5张连续,不能含2和王)
|
|
if n >= 5 and max(values) <= 14: # A=14
|
|
if len(unique_values) == n and unique_values[-1] - unique_values[0] == n - 1 and counts == [1] * n:
|
|
return {
|
|
'type': 'straight',
|
|
'value': unique_values[0],
|
|
'length': n,
|
|
'name': f'{n}连顺',
|
|
'cards': cards
|
|
}
|
|
|
|
# 连对 (>=3对连续)
|
|
if n >= 6 and n % 2 == 0 and counts == [2] * (n // 2):
|
|
pair_values = [v for v, c in value_count.items() if c == 2]
|
|
if len(pair_values) == n // 2 and max(pair_values) <= 14:
|
|
if pair_values[-1] - pair_values[0] == len(pair_values) - 1:
|
|
return {
|
|
'type': 'double_straight',
|
|
'value': pair_values[0],
|
|
'length': n // 2,
|
|
'name': f'{n // 2}连对',
|
|
'cards': cards
|
|
}
|
|
|
|
# 飞机 (>=2组连续三张)
|
|
if n >= 6 and n % 3 == 0:
|
|
triple_values = [v for v, c in value_count.items() if c == 3]
|
|
if len(triple_values) == n // 3 and max(triple_values) <= 14:
|
|
if triple_values[-1] - triple_values[0] == len(triple_values) - 1:
|
|
return {
|
|
'type': 'triple_straight',
|
|
'value': triple_values[0],
|
|
'length': n // 3,
|
|
'name': f'{n // 3}连三张',
|
|
'cards': cards
|
|
}
|
|
|
|
# 四带二
|
|
if n == 6 and counts == [4, 1, 1]:
|
|
quad_value = [v for v, c in value_count.items() if c == 4][0]
|
|
return {
|
|
'type': 'quad_with_two',
|
|
'value': quad_value,
|
|
'length': 6,
|
|
'name': '四带二',
|
|
'cards': cards
|
|
}
|
|
|
|
if n == 8 and counts == [4, 2, 2]:
|
|
quad_value = [v for v, c in value_count.items() if c == 4][0]
|
|
return {
|
|
'type': 'quad_with_two',
|
|
'value': quad_value,
|
|
'length': 8,
|
|
'name': '四带两对',
|
|
'cards': cards
|
|
}
|
|
|
|
return None
|
|
|
|
|
|
def can_beat(desktop_type: Optional[Dict], new_type: Dict) -> bool:
|
|
"""
|
|
判断新牌型能否管上桌面牌型
|
|
type字段现在是字符串而非CardType枚举
|
|
"""
|
|
if not desktop_type:
|
|
return True
|
|
|
|
new_type_str = new_type['type']
|
|
desktop_type_str = desktop_type['type']
|
|
|
|
# 王炸最大
|
|
if new_type_str == 'joker_bomb':
|
|
return True
|
|
if desktop_type_str == 'joker_bomb':
|
|
return False
|
|
|
|
# 炸弹 > 非炸弹
|
|
if new_type_str == 'bomb' and desktop_type_str != 'bomb':
|
|
return True
|
|
if desktop_type_str == 'bomb' and new_type_str != 'bomb':
|
|
return False
|
|
|
|
# 同类型比较
|
|
if new_type_str != desktop_type_str:
|
|
return False
|
|
|
|
# 连牌数量必须相同
|
|
if new_type.get('length') != desktop_type.get('length'):
|
|
return False
|
|
|
|
# 比较牌值
|
|
return new_type['value'] > desktop_type['value']
|
|
|
|
|
|
def evaluate_hand_strength(cards: List[Card]) -> int:
|
|
"""
|
|
评估手牌强度(用于叫地主决策)
|
|
返回分数,越高表示手牌越好
|
|
"""
|
|
score = 0
|
|
|
|
# 大牌加分
|
|
for card in cards:
|
|
if card.value >= 16: # 王
|
|
score += 4
|
|
elif card.value == 15: # 2
|
|
score += 3
|
|
elif card.value == 14: # A
|
|
score += 2
|
|
elif card.value == 13: # K
|
|
score += 1
|
|
|
|
# 炸弹加分
|
|
value_count = {}
|
|
for c in cards:
|
|
value_count[c.value] = value_count.get(c.value, 0) + 1
|
|
|
|
for count in value_count.values():
|
|
if count == 4:
|
|
score += 6
|
|
|
|
# 王炸
|
|
jokers = [c for c in cards if c.is_joker]
|
|
if len(jokers) == 2:
|
|
score += 10
|
|
|
|
return score
|
|
|
|
|
|
def cards_to_dicts(cards: List[Card]) -> List[Dict]:
|
|
"""转换为字典列表"""
|
|
return [c.to_dict() for c in cards]
|
|
|
|
|
|
def find_cards_by_ids(all_cards: List[Card], card_ids: List[int]) -> List[Card]:
|
|
"""通过ID查找牌"""
|
|
card_map = {c.id: c for c in all_cards}
|
|
return [card_map.get(cid) for cid in card_ids if cid in card_map]
|
|
|
|
|
|
# 导出
|
|
CARD_TYPES = CardType
|