Files
WPSBot/games/sgs_core.py
借我清欢与鹤梦 86f87b440e 新增三国杀系统
2025-10-30 16:19:02 +08:00

530 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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