"""三国杀游戏核心逻辑模块""" import logging from typing import List, Dict, Optional, Set from enum import Enum from dataclasses import dataclass, field import random logger = logging.getLogger(__name__) class CardType(Enum): """卡牌类型""" BASIC = "基本牌" TRICK = "锦囊牌" EQUIPMENT = "装备牌" class CardSuit(Enum): """卡牌花色""" SPADE = "♠" # 黑桃 HEART = "♥" # 红桃 CLUB = "♣" # 梅花 DIAMOND = "♦" # 方块 class CardColor(Enum): """卡牌颜色""" RED = "红色" BLACK = "黑色" class Role(Enum): """角色身份""" LORD = "主公" LOYAL = "忠臣" REBEL = "反贼" SPY = "内奸" class Phase(Enum): """回合阶段""" PREPARE = "准备阶段" JUDGE = "判定阶段" DRAW = "摸牌阶段" PLAY = "出牌阶段" DISCARD = "弃牌阶段" END = "结束阶段" @dataclass class Card: """卡牌""" name: str # 卡牌名称 card_type: CardType # 卡牌类型 suit: CardSuit # 花色 number: int # 点数 (1-13) description: str = "" # 描述 @property def color(self) -> CardColor: """获取卡牌颜色""" if self.suit in [CardSuit.HEART, CardSuit.DIAMOND]: return CardColor.RED return CardColor.BLACK def __str__(self) -> str: """字符串表示""" return f"{self.suit.value}{self.number} {self.name}" def to_dict(self) -> Dict: """转换为字典""" return { "name": self.name, "type": self.card_type.value, "suit": self.suit.value, "number": self.number, "color": self.color.value, "description": self.description } @dataclass class Skill: """技能""" name: str # 技能名称 description: str # 技能描述 skill_type: str = "主动" # 技能类型: 主动/锁定/限定/觉醒 def to_dict(self) -> Dict: """转换为字典""" return { "name": self.name, "description": self.description, "type": self.skill_type } @dataclass class General: """武将""" name: str # 武将名称 max_hp: int # 体力上限 skills: List[Skill] # 技能列表 kingdom: str = "魏" # 势力 def to_dict(self) -> Dict: """转换为字典""" return { "name": self.name, "max_hp": self.max_hp, "kingdom": self.kingdom, "skills": [skill.to_dict() for skill in self.skills] } @dataclass class Player: """玩家""" user_id: int # 用户ID username: str # 用户名 general: Optional[General] = None # 武将 role: Optional[Role] = None # 身份 hp: int = 0 # 当前体力 hand_cards: List[Card] = field(default_factory=list) # 手牌 equipment: Dict[str, Card] = field(default_factory=dict) # 装备区 judge_area: List[Card] = field(default_factory=list) # 判定区 is_alive: bool = True # 是否存活 is_chained: bool = False # 是否横置 def __post_init__(self): """初始化后处理""" if self.general and self.hp == 0: self.hp = self.general.max_hp @property def hand_count(self) -> int: """手牌数量""" return len(self.hand_cards) @property def attack_range(self) -> int: """攻击距离""" weapon = self.equipment.get("武器") if weapon: # 根据武器名称返回距离 weapon_ranges = { "诸葛连弩": 1, "青釭剑": 2, "雌雄双股剑": 2, "青龙偃月刀": 3, "丈八蛇矛": 3, "方天画戟": 4, "麒麟弓": 5 } return weapon_ranges.get(weapon.name, 1) return 1 def add_card(self, card: Card): """添加手牌""" self.hand_cards.append(card) def remove_card(self, card: Card) -> bool: """移除手牌""" if card in self.hand_cards: self.hand_cards.remove(card) return True return False def take_damage(self, damage: int) -> bool: """受到伤害 Returns: 是否死亡 """ self.hp -= damage if self.hp <= 0: self.is_alive = False return True return False def recover(self, amount: int): """回复体力""" if self.general: self.hp = min(self.hp + amount, self.general.max_hp) def to_dict(self) -> Dict: """转换为字典""" return { "user_id": self.user_id, "username": self.username, "general": self.general.to_dict() if self.general else None, "role": self.role.value if self.role else None, "hp": self.hp, "max_hp": self.general.max_hp if self.general else 0, "hand_count": self.hand_count, "equipment": {k: v.to_dict() for k, v in self.equipment.items()}, "is_alive": self.is_alive, "is_chained": self.is_chained } class CardDeck: """牌堆""" def __init__(self): """初始化牌堆""" self.cards: List[Card] = [] self.discard_pile: List[Card] = [] self._init_standard_deck() def _init_standard_deck(self): """初始化标准牌堆(简化版)""" # 杀 (30张) for suit, numbers in [ (CardSuit.SPADE, [7, 8, 8, 9, 9, 10, 10]), (CardSuit.CLUB, [2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11]), (CardSuit.HEART, [10, 10, 11]), (CardSuit.DIAMOND, [6, 7, 8, 9, 10, 13]) ]: for num in numbers: self.cards.append(Card("杀", CardType.BASIC, suit, num, "对攻击范围内的一名角色造成1点伤害")) # 闪 (15张) for suit, numbers in [ (CardSuit.HEART, [2, 2, 13]), (CardSuit.DIAMOND, [2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11]) ]: for num in numbers: self.cards.append(Card("闪", CardType.BASIC, suit, num, "抵消一张【杀】的效果")) # 桃 (8张) for suit, numbers in [ (CardSuit.HEART, [3, 4, 6, 7, 8, 9, 12]), (CardSuit.DIAMOND, [12]) ]: for num in numbers: self.cards.append(Card("桃", CardType.BASIC, suit, num, "回复1点体力")) # 锦囊牌 # 无懈可击 (3张) for suit, num in [(CardSuit.SPADE, 11), (CardSuit.CLUB, 12), (CardSuit.CLUB, 13)]: self.cards.append(Card("无懈可击", CardType.TRICK, suit, num, "抵消一张锦囊牌的效果")) # 决斗 (3张) for suit, num in [(CardSuit.SPADE, 1), (CardSuit.CLUB, 1), (CardSuit.DIAMOND, 1)]: self.cards.append(Card("决斗", CardType.TRICK, suit, num, "与目标角色拼点,失败者受到1点伤害")) # 过河拆桥 (6张) for suit, numbers in [ (CardSuit.SPADE, [3, 4, 12]), (CardSuit.CLUB, [3, 4]), (CardSuit.HEART, [12]) ]: for num in numbers: self.cards.append(Card("过河拆桥", CardType.TRICK, suit, num, "弃置目标角色的一张牌")) # 顺手牵羊 (5张) for suit, numbers in [ (CardSuit.SPADE, [3, 4, 11]), (CardSuit.DIAMOND, [3, 4]) ]: for num in numbers: self.cards.append(Card("顺手牵羊", CardType.TRICK, suit, num, "获得目标角色的一张牌")) # 南蛮入侵 (3张) for suit, num in [(CardSuit.SPADE, 7), (CardSuit.SPADE, 13), (CardSuit.CLUB, 7)]: self.cards.append(Card("南蛮入侵", CardType.TRICK, suit, num, "所有其他角色需打出【杀】,否则受到1点伤害")) # 万箭齐发 (1张) self.cards.append(Card("万箭齐发", CardType.TRICK, CardSuit.HEART, 1, "所有其他角色需打出【闪】,否则受到1点伤害")) # 桃园结义 (1张) self.cards.append(Card("桃园结义", CardType.TRICK, CardSuit.HEART, 1, "所有角色回复1点体力")) # 五谷丰登 (2张) for num in [3, 4]: self.cards.append(Card("五谷丰登", CardType.TRICK, CardSuit.HEART, num, "所有角色依次获得一张牌")) # 装备牌(简化版,只添加几种) # 诸葛连弩 self.cards.append(Card("诸葛连弩", CardType.EQUIPMENT, CardSuit.CLUB, 1, "武器,攻击范围1,出牌阶段可以使用任意张【杀】")) self.cards.append(Card("诸葛连弩", CardType.EQUIPMENT, CardSuit.DIAMOND, 1, "武器,攻击范围1,出牌阶段可以使用任意张【杀】")) # 青釭剑 self.cards.append(Card("青釭剑", CardType.EQUIPMENT, CardSuit.SPADE, 6, "武器,攻击范围2,无视目标防具")) # 八卦阵 self.cards.append(Card("八卦阵", CardType.EQUIPMENT, CardSuit.SPADE, 2, "防具,判定为红色时视为使用了【闪】")) self.cards.append(Card("八卦阵", CardType.EQUIPMENT, CardSuit.CLUB, 2, "防具,判定为红色时视为使用了【闪】")) # 的卢 self.cards.append(Card("的卢", CardType.EQUIPMENT, CardSuit.CLUB, 5, "+1马,其他角色计算与你的距离+1")) # 赤兔 self.cards.append(Card("赤兔", CardType.EQUIPMENT, CardSuit.HEART, 5, "-1马,你计算与其他角色的距离-1")) # 洗牌 self.shuffle() def shuffle(self): """洗牌""" random.shuffle(self.cards) logger.info(f"牌堆已洗牌,共 {len(self.cards)} 张牌") def draw(self, count: int = 1) -> List[Card]: """摸牌 Args: count: 摸牌数量 Returns: 摸到的牌列表 """ if len(self.cards) < count: # 牌不够,将弃牌堆洗入牌堆 self.cards.extend(self.discard_pile) self.discard_pile.clear() self.shuffle() drawn = self.cards[:count] self.cards = self.cards[count:] return drawn def discard(self, cards: List[Card]): """弃牌""" self.discard_pile.extend(cards) class GeneralPool: """武将池""" def __init__(self): """初始化武将池""" self.generals: List[General] = [] self._init_standard_generals() def _init_standard_generals(self): """初始化标准武将(简化版)""" # 刘备 self.generals.append(General( name="刘备", max_hp=4, kingdom="蜀", skills=[ Skill("仁德", "出牌阶段,你可以将任意张手牌交给其他角色,若你给出的牌达到两张或更多,你回复1点体力", "主动"), Skill("激将", "主公技,当你需要使用或打出【杀】时,你可以令其他蜀势力角色打出一张【杀】(视为由你使用或打出)", "主动") ] )) # 关羽 self.generals.append(General( name="关羽", max_hp=4, kingdom="蜀", skills=[ Skill("武圣", "你可以将一张红色牌当【杀】使用或打出", "主动") ] )) # 张飞 self.generals.append(General( name="张飞", max_hp=4, kingdom="蜀", skills=[ Skill("咆哮", "锁定技,出牌阶段,你使用【杀】无次数限制", "锁定") ] )) # 诸葛亮 self.generals.append(General( name="诸葛亮", max_hp=3, kingdom="蜀", skills=[ Skill("观星", "准备阶段,你可以观看牌堆顶的X张牌(X为存活角色数且至多为5),将任意数量的牌置于牌堆顶,其余的牌置于牌堆底", "主动"), Skill("空城", "锁定技,当你没有手牌时,你不能成为【杀】或【决斗】的目标", "锁定") ] )) # 赵云 self.generals.append(General( name="赵云", max_hp=4, kingdom="蜀", skills=[ Skill("龙胆", "你可以将【杀】当【闪】、【闪】当【杀】使用或打出", "主动") ] )) # 曹操 self.generals.append(General( name="曹操", max_hp=4, kingdom="魏", skills=[ Skill("奸雄", "当你受到伤害后,你可以获得对你造成伤害的牌", "主动"), Skill("护驾", "主公技,当你需要使用或打出【闪】时,你可以令其他魏势力角色打出一张【闪】(视为由你使用或打出)", "主动") ] )) # 司马懿 self.generals.append(General( name="司马懿", max_hp=3, kingdom="魏", skills=[ Skill("反馈", "当你受到1点伤害后,你可以获得伤害来源的一张牌", "主动"), Skill("鬼才", "在任意角色的判定牌生效前,你可以打出一张手牌代替之", "主动") ] )) # 夏侯惇 self.generals.append(General( name="夏侯惇", max_hp=4, kingdom="魏", skills=[ Skill("刚烈", "当你受到伤害后,你可以进行判定:若结果不为♥,则伤害来源选择一项:弃置两张手牌,或受到你造成的1点伤害", "主动") ] )) # 甄姬 self.generals.append(General( name="甄姬", max_hp=3, kingdom="魏", skills=[ Skill("洛神", "准备阶段,你可以进行判定:当黑色判定牌生效后,你获得之。若结果为黑色,你可以重复此流程", "主动"), Skill("倾国", "你可以将一张黑色手牌当【闪】使用或打出", "主动") ] )) # 孙权 self.generals.append(General( name="孙权", max_hp=4, kingdom="吴", skills=[ Skill("制衡", "出牌阶段限一次,你可以弃置任意张牌,然后摸等量的牌", "主动"), Skill("救援", "主公技,锁定技,其他吴势力角色对你使用【桃】时,该角色摸一张牌", "锁定") ] )) # 周瑜 self.generals.append(General( name="周瑜", max_hp=3, kingdom="吴", skills=[ Skill("英姿", "摸牌阶段,你可以额外摸一张牌", "主动"), Skill("反间", "出牌阶段限一次,你可以令一名其他角色选择一种花色,然后该角色获得你的一张手牌并展示之,若此牌与所选花色不同,你对其造成1点伤害", "主动") ] )) # 吕蒙 self.generals.append(General( name="吕蒙", max_hp=4, kingdom="吴", skills=[ Skill("克己", "若你于出牌阶段内没有使用或打出过【杀】,你可以跳过此回合的弃牌阶段", "主动") ] )) # 黄盖 self.generals.append(General( name="黄盖", max_hp=4, kingdom="吴", skills=[ Skill("苦肉", "出牌阶段,你可以失去1点体力,然后摸两张牌", "主动") ] )) # 吕布 self.generals.append(General( name="吕布", max_hp=4, kingdom="群", skills=[ Skill("无双", "锁定技,当你使用【杀】指定一个目标后,该角色需依次使用两张【闪】才能抵消此【杀】;当你使用【决斗】指定一个目标后,该角色每次响应此【决斗】需依次打出两张【杀】", "锁定") ] )) # 貂蝉 self.generals.append(General( name="貂蝉", max_hp=3, kingdom="群", skills=[ Skill("离间", "出牌阶段限一次,你可以弃置一张牌并选择两名男性角色,后选择的角色视为对先选择的角色使用一张【决斗】(此【决斗】不能被【无懈可击】响应)", "主动"), Skill("闭月", "结束阶段,你可以摸一张牌", "主动") ] )) # 华佗 self.generals.append(General( name="华佗", max_hp=3, kingdom="群", skills=[ Skill("急救", "你的回合外,你可以将一张红色牌当【桃】使用", "主动"), Skill("青囊", "出牌阶段限一次,你可以弃置一张手牌并令一名角色回复1点体力", "主动") ] )) def get_random_generals(self, count: int, exclude: List[str] = None) -> List[General]: """随机获取武将 Args: count: 数量 exclude: 排除的武将名称列表 Returns: 武将列表 """ available = [g for g in self.generals if not exclude or g.name not in exclude] if len(available) < count: return available return random.sample(available, count) def get_general_by_name(self, name: str) -> Optional[General]: """根据名称获取武将""" for general in self.generals: if general.name == name: return general return None