530 lines
18 KiB
Python
530 lines
18 KiB
Python
"""三国杀游戏核心逻辑模块"""
|
||
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
|
||
|