【待办】三国杀单挑测试脚本

序言

前天晚上开始写之前一直想写的三国杀单挑仿真项目,初始灵感是想要测试四血界孙权单挑四血新王异在不同状态下的优劣(如新王异零装备起手,单+1马起手,+1马和藤甲/仁王盾起手,单雌雄剑起手,单木牛流马起手),后续是想使用强化学习方法进行训练得出界孙权的最优打法,之前听说过四血标孙权可以和四血新王异五五开,以为界孙权的容错会相对高一些,但是看了去年半个橙子上传的老炮杯一局经典的界孙权内奸单挑主公新王异的对局,看下来界孙权实在是太被动了(当然那时候新王异已经神装雌雄+木马了,普遍认为界孙权大劣,但是最后还是界孙权竟然没有靠闪电就完成翻盘,虽然只是险胜。我个人的感觉是王异只要有雌雄剑和+1马基本上就可以一战了,如果摸到木牛流马王异几乎必胜),这个单挑对于界孙权来说要比想象中的困难得多,因为能针对新王异的卡牌就只有三张乐不思蜀,而且木马本质上对于界孙权来说只能存一张牌。

个人觉得能打赢四血新王异的界孙权才配称为是会玩的界孙权,抛开界孙权明显处于过牌或被强控而处于劣势,而只能选择对爆的单挑对局(许攸,曹纯,周舫等),或是明显优势无需探讨的对局,笔者认为比较有趣的应该是与四血曹仁和四血王异之间的单挑,四血界孙权到底应该以何种策略应对,所以很想知道真正会玩的界孙权的最优策略到底是什么样子的。

后来开始写发现需要定义的规则实在是太多了… 即便是写两个白板之间的单挑也需要大量时间,笔者的思路是先定义白板武将作为父类,类中定义武将的体力值,血量值,手牌区,装备区,判定区的卡牌,以及各个摸牌,出牌,弃牌等阶段,以及如何使用卡牌的定义,然后定义孙权和王异分别来继承这个白板类。可以重写相关函数的定义。

目前大致完成了单挑的定义,以及除锦囊牌外其他卡牌的使用定义,尚未完成几个武器的用法定义,以及出牌阶段的规则还没有完善。本来以为能周末偷闲写完,现在看来可能只能暂时烂尾了,以后有时间,还有热情的话再回来写写。其实还是很希望能有人能写出一个可以用于单挑测试的脚本的,毕竟依靠手动测试实在是很费时间,而界孙权的用法实际上又很难通过有限的规则去定义出来,尤其如果将弃牌堆的卡牌也作为特征输入,机器就可以对剩余牌堆中的卡牌作出预判,甚至可以对新王异的手牌作出预判,很多时候对界孙权制衡的选择有很大的影响的。而牌堆加入木牛流马又会极大地改变对局地走向。当然其实强化学习的起点可以直接从界孙权无脑全制衡开始,逐渐去逼近最优的解法,可惜现在连游戏规则都定义不全,主要是感觉花不起这么长时间写个与本业无关的东西了… 又浪费了两天时间做了件蠢事[Facepalm]

代码挂着做个备份,有缘回头再来完善。



code(To Be Done)

以下各文件置于同一目录即可,主脚本为example_game.py

default_deck.txt

[方片]
1|诸葛连弩|决斗|朱雀羽扇
2|闪|闪|桃
3|闪|顺手牵羊|桃
4|闪|顺手牵羊|火杀
5|闪|贯石斧|火杀|木牛流马
6|杀|闪|闪
7|杀|闪|闪
8|杀|闪|闪
9|杀|闪|酒
10|杀|闪|闪
11|闪|闪|闪
12|桃|方天画戟|火攻|无懈可击
13|杀|紫骍|骅骝
[梅花]
1|决斗|诸葛连弩|白银狮子
2|杀|八卦阵|藤甲|仁王盾
3|杀|过河拆桥|酒
4|杀|过河拆桥|兵粮寸断
5|杀|的卢|雷杀
6|杀|乐不思蜀|雷杀
7|杀|南蛮入侵|雷杀
8|杀|杀|雷杀
9|杀|杀|酒
10|杀|杀|铁索连环
11|杀|杀|铁索连环
12|借刀杀人|无懈可击|铁索连环
13|借刀杀人|无懈可击|铁索连环
[红心]
1|桃园结义|万箭齐发|无懈可击
2|闪|闪|火攻
3|桃|五谷丰登|火攻
4|桃|五谷丰登|火杀
5|麒麟弓|赤兔|桃
6|桃|乐不思蜀|桃
7|桃|无中生有|火杀
8|桃|无中生有|闪
9|桃|无中生有|闪
10|杀|杀|火杀
11|杀|无中生有|闪
12|桃|过河拆桥|闪|闪电
13|闪|爪黄飞电|无懈可击
[黑桃]
1|决斗|闪电|古锭刀
2|雌雄双股剑|八卦阵|藤甲|寒冰剑
3|过河拆桥|顺手牵羊|酒
4|过河拆桥|顺手牵羊|雷杀
5|青龙偃月刀|绝影|雷杀
6|乐不思蜀|青釭剑|雷杀
7|杀|南蛮入侵|雷杀
8|杀|杀|雷杀
9|杀|杀|酒
10|杀|杀|兵粮寸断
11|顺手牵羊|无懈可击|铁索连环
12|过河拆桥|丈八蛇矛|铁索连环
13|南蛮入侵|大宛|无懈可击

example_base.py

# -*- coding: UTF-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

import random

from example_utils import *
from example_config import CardConfig

class BaseCard(object):
	
	def __init__(self, **kwargs) -> None:
		"""
		卡牌基类;
		:param id:				必选字段[int], 卡牌编号;
		:param name:			必选字段[str], 卡牌名称;
		:param first_class:		必选字段[str], 卡牌一级类别, 取值范围('基本牌', '锦囊牌', '装备牌');
		:param second_class:	必选字段[str], 卡牌二级类别('普通锦囊牌', '延时锦囊牌', '武器牌', '防具牌', '进攻马', '防御马', '宝物牌');
		:param area:			可选字段[str], 卡牌所在区域, 取值范围('手牌区', '装备区', '判定区', '弃牌区', '牌堆区', '结算区', '木牛流马区'), 默认值`CardConfig.deck_area`表示卡牌初始位于牌堆;
		:param suit:			可选字段[str], 卡牌花色, 取值范围('黑桃', '红心', '梅花', '方片'), 默认值None表示无花色;
		:param point:			可选字段[int], 卡牌点数, 取值范围(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13), 默认值None表示无点数;
		:param attack_range:	可选字段[int], 武器牌攻击范围, 取值范围(1, 2, 3, 4, 5), 默认值None表示非武器牌;
		"""
		self.id = kwargs.get('id')										 # 设置卡牌编号
		self.name = kwargs.get('name')									 # 设置卡牌名称
		self.first_class = kwargs.get('first_class')					 # 设置卡牌一级分类
		self.second_class = kwargs.get('second_class')					 # 设置卡牌二级分类
		self.suit = kwargs.get('suit', None)							 # 设置卡牌花色
		self.point = kwargs.get('point', None)							 # 设置卡牌点数
		self.attack_range = kwargs.get('attack_range', None)			 # 设置武器的攻击范围
		self.components = None											 # 这个是用于将多张牌当作一张牌使用的情形, 如丈八蛇矛拍出两张牌当作杀使用, 类型为list, 默认值None表示非合成卡牌;

class BaseCharacter(object):
	
	def __init__(self, **kwargs) -> None:
		"""
		角色基类;
		:param id:					必选字段[int], 卡牌编号;
		:param health_point_max:	可选字段[int], 体力上限;
		:param health_point:		可选字段[int], 体力值;
		:param is_male:				可选字段[bool], 是否为男性角色;
		"""
		self.id = kwargs.get('id')										 # 设置角色编号
		self.health_point = kwargs.get('health_point', 5)				 # 设置体力值
		self.health_point_max = kwargs.get('health_point_max', 5)		 # 设置体力上限
		self.is_male = kwargs.get('is_male', True)						 # 设置性别
		self.hand_area = []												 # 手牌区
		self.equipment_area = {											 # 初始化装备区
			CardConfig.arms: None,										 # 初始化武器为空
			CardConfig.armor: None,										 # 初始化防具为空
			CardConfig.defend_horse: None,								 # 初始化防御马为空
			CardConfig.attack_horse: None,								 # 初始化进攻马为空
			CardConfig.treasure: None,									 # 初始化宝物
		}														
		self.judgment_area = []											 # 初始化判定区为空: 这里之所以用list作为判定区而非dict, 是因为判定区的卡牌是有序的
		self.mnlm_area = None											 # 初始化木牛流马区是不存在的
		self.total_liquor = 0											 # 初始化酒数为0, 每有一口酒, 下一张杀的伤害+1, 酒的效果在回合结束阶段清零
		self.kill_times = 1												 # 初始化可使用的杀的次数为1
		self.is_masked = False											 # 初始状态: 未翻面
		self.is_bound = False											 # 初始状态: 未横置
		self.is_alive = True											 # 初始状态: 存活
		

	def round_start_phase(self, deck) -> None:
		"""回合开始阶段"""
		pass

	def preparation_phase(self, deck) -> None:
		"""准备阶段"""
		pass

	def judgment_phase(self, game) -> dict:
		"""判定阶段"""
		judgment_result = {
			CardConfig.lbss: None,										 # 乐不思蜀判定结果
			CardConfig.blcd: None,										 # 兵粮寸断判定结果
			CardConfig.lightning: None,									 # 闪电判定结果
		}
		for card in self.judgment_area[::-1]:							 # 判定顺序服从后来先判
			flag = game.wxkj_polling(card, mode='manual')
			if flag:													 # 轮询结果是生效
				card_name = card.name									 # 获取判定牌名称: 乐不思蜀, 兵粮寸断, 闪电;
				judge = fetch_cards(deck=game.deck, number=1)[0]		 # 获得牌堆顶第一张牌
				judgment_result[card_name] = judge						 # 返回判定区的延时类锦囊牌与判定结果牌
			else:
				game.deck.discards.append(card)
		return judgment_result

		
	def draw_phase(self, deck, number: int=2) -> None:
		"""摸牌阶段"""
		cards = fetch_cards(deck=deck, number=number)					 # 从牌堆获得卡牌
		self.hand_area.extend(cards)									 # 将卡牌加入到手牌区

	def play_phase(self, game, mode: str='manual') -> None:
		"""出牌阶段"""
		# 定义出杀次数
		arms = self.equipment_area[CardConfig.arms]
		if arms is not None and arms.name == CardConfig.zgln:
			self.kill_times = 999
		else:
			self.kill_times = 1
		while True:
			card_ids = [card_id for card_id in self.hand_area]
			if mode == 'manual':
				ans = input('  - 请输入需要使用的卡牌编号({}): '.format(card_ids))
				if not ans.isdigit():
					print('    + 出牌阶段结束!')
					break
				ans = int(ans)
				if not ans in wxkj_ids:
					print('    + 输入编号不在手牌中!')
			else:
				ans = random.sample(card_ids, 1)[0]
			
		
	def fold_phase(self, deck, discards: list=None) -> None:
		"""弃牌阶段"""
		overflow = len(self.hand_area) - self.health_point				 # 计算手牌溢出数量
		overflow = max(overflow, 0)										 # 手牌溢出最小为0
		# 情况一: 如果没有给定需要弃置的卡牌: 即处于托管状态
		if discards is None:
			if overflow > 0:											 # 如果存在手牌溢出
				random.shuffle(self.hand_area)							 # 打乱手牌区卡牌
				auto_discards = []										 # 随机存储托管弃牌										
				for _ in range(overflow):								 # 弃置溢出的卡牌
					auto_discards.append(self.hand_area.pop(0))			 # 直接从随机从手牌区的起始位置弹出卡牌即可
				deck.discards.extend(auto_discards)						 # 将弃置的卡牌置入弃牌堆
		# 情况二: 如果给定了需要弃置的卡牌
		else:															
			assert len(discards) == overflow, '需要弃置的卡牌数量不正确'	 # 否则要求给定需要弃置的卡牌必须与手牌溢出数量相等
			card_ids = []
			# 检验弃置的卡牌都可以在手牌区找到
			for discard in discards:									 # 遍历每张需要弃置的卡牌
				card_ids.append(discard.id)								 # 记录弃置卡牌的编号
				for card in self.hand_area:								 # 确定其在手牌区中的位置
					in_hand = False										 # 判断其是否在手牌区中的标识
					# if discard.name == card.name and \
					# 	discard.point == card.point and \
					# 	discard.suit == card.suit:						 # 根据卡牌名称, 卡牌点数, 卡牌花色唯一确定
					if discard.id == card.id:							 # 根据卡牌编号唯一确定
						in_hand = True									 # 在手牌区中找到该卡牌
						break
				assert in_hand, '需要弃置的卡牌不在手牌区'					 # 找不到需要弃置的卡牌则抛出异常
			# 删除需要弃置的卡牌
			index = -1													 # 记录弃置卡牌的位置discards是相同的
			discards_copy = []											 # 仅用于验证, 其结果应当与discards相同
			for card in self.hand_area[:]:								 # 从手牌区依次删除弃置的卡牌
				index += 1
				if card.id in card_ids:									 # 如果当前卡牌在discards的卡牌编号中
					discards_copy(self.hand_area.pop(index))			 # 删除手牌区的卡牌
					index -= 1											 # 删除卡牌后需要将index减一
			assert len(discards_copy) == len(discards), '意外的错误'		 # 确认discards_copy与discards所包含的卡牌数量相等
				
	def ending_phase(self, deck) -> None: 
		"""结束阶段"""
		pass

	def round_end_phase(self, deck) -> None:
		"""回合结束阶段"""
		self.liquor_count = 0											 # 结束阶段将酒数重置
		arms = self.equipment_area[CardConfig.arms]
		if arms is not None and arms.name == CardConfig.zgln:
			self.kill_times = 999
		else:
			self.kill_times = 1

	####################################################################

	def is_wxkj_exist(self) -> bool:
		"""判断是否有无懈可击可以使用"""
		for card in self.hand_area:
			if card.name == CardConfig.wxkj:
				return True
		return False
		
	def get_all_wxkjs(self) -> list:
		"""获取所有无懈可击卡牌"""
		wxkjs = []
		for card in self.hand_area:
			if card.name == CardConfig.wxkj:
				wxkjs.append(card)
		return wxkjs		
	
	def get_all_peaches(self) -> list:
		"""获取所有桃"""
		peaches = []
		for card in self.hand_area:
			if card.name == CardConfig.peach:
				peaches.append(card)
		return peaches

	def get_all_dodges(self) -> list:
		"""获取所有闪"""
		dodges = []
		for card in self.hand_area:
			if card.name == CardConfig.dodge:
				dodges.append(card)
		return dodges

	def ask_for_peach(self, deck, mode: str='manual') -> bool:
		"""求桃: 单挑模式限定自救"""
		assert self.health_point <= 0
		for i in range(1 - self.health_point):							 # 需要求若干次桃
			peaches = self.get_all_peaches()							 # 获取当前手牌区所有
			peach_ids = [peach.id for peach in peaches]
			print('可使用的桃子如下(目前还有{}个桃): '.format(len(peaches)))
			for peach in peaches:
				print('  - ', end='')
				display_card(peach)
			
			if len(peaches) == 0:
				ans = 0
			else:
				if mode == 'manual':
					while True:	
						ans = input('  - 是否使用桃?(|是->1|否->0|)')
						if not ans.isdigit():
							print('    + 请按照要求正确输入!')
							continue
						ans = int(ans)
						if not ans in [0, 1]:
							print('    + 请按照要求正确输入!')
						break
				else:
					ans = random.randint(0, 1)
			if ans == 1:
				if mode == 'manual':
					while True:
						ans = input('  - 请输入需要使用的桃的卡牌编号({}): '.format(peach_ids))
						if not ans.isdigit():
							print('    + 请按照要求正确输入!')
							continue
						ans = int(ans)
						if not ans in wxkj_ids:
							print('    + 输入编号不在手牌中!')
						break
				else:
					random.sample(peach_ids, 1)[0]
				# 使用掉手牌区的桃子
				for i in range(len(self.hand_area)):
					if self.hand_area[i].id == ans:
						self.request(deck, self.hand_area.pop(i))
			else:
				raise Exception('玩家死亡!')

	####################################################################

	def request(self, deck, card, targets: list=None, mode: str='manual', **kwargs) -> dict:
		"""
		请求卡牌: 即使用卡牌;
		:param deck: 当前牌堆;
		:param card: 使用的卡牌;
		:param targets: 卡牌使用的对象, 列表元素类型为`BaseCharacter`, 有些卡牌无需指定使用对象, 因为只能对自己使用(如所有装备牌, 酒, 闪电, 无中生有);
		:param mode: 操作模式, 默认手动操作;
		:param kwargs: 其他可能的参数;
		"""
		card_name = card.name											 # 获取卡牌名称
		card_first_class = card.first_class								 # 获取卡牌一级分类
		card_second_class = card.second_class							 # 获取卡牌二级分类
		if card_first_class == CardConfig.basic:						 # 使用基本牌
			if card_name in [CardConfig.kill, CardConfig.fire_kill, CardConfig.thunder_kill]: # [杀]
				assert len(targets) == 1								 # 杀的目标只能有一个
				assert not targets[0].id == self.id						 # 杀不能对自己使用
				distance = calc_distance(source_character=self, target_character=targets[0], base=1)
				attack_range = calc_attack_range(character=self)
				if attack_range < distance:
					raise Exception('距离不够无法使用杀!')
				armor = targets[0].equipment_area[CardConfig.armor]
				arms = self.equipment_area[CardConfig.arms]
				if arms is not None:
					# 朱雀羽扇
					if arms == CardConfig.zqys:
						if mode == 'manual':
							while True:
								ans = input('是否使用朱雀羽扇?(|是->1|否->0|)')
								if not ans.isdigit():
									print('    + 请按照要求正确输入!')
									continue
								ans = int(ans)
								if not ans in [0, 1]:
									print('    + 请按照要求正确输入!')
									continue
								break
						else:
							ans = random.randint(0, 1)			
						if ans == 1:									 # 使用朱雀羽扇
							card_name == CardConfig.fire_kill			 # 卡牌变为火杀
					# 雌雄双股剑
					if arms == CardConfig.cxsgj:
						if not self.is_male == targets[0].is_male:		 # 性别不同
							if mode == 'manual':
								while True:
									ans = input('是否使用雌雄双股剑?(|是->1|否->0|)')
									if not ans.isdigit():
										print('    + 请按照要求正确输入!')
										continue
									ans = int(ans)
									if not ans in [0, 1]:
										print('    + 请按照要求正确输入!')
										continue
									break
							else:
								ans = random.randint(0, 1)			
							if ans == 1:									 # 使用雌雄双股剑
								if len(target.hand_area) == 0:				 # 手牌区为空则必须选择摸牌
									ans = 0
								if mode == 'manual':
									while True:
										ans = input('自己弃牌或是让对方摸牌?(|弃牌->1|摸牌->0|)')
										if not ans.isdigit():
											print('    + 请按照要求正确输入!')
											continue
										ans = int(ans)
										if not ans in [0, 1]:
											print('    + 请按照要求正确输入!')
											continue
										break											
								else:
									ans = random.randint(0, 1)		
								if ans == 1:								 # 弃牌
									card_ids = [card_id for card_id in targets[0].hand_area]
									if mode == 'manual':
										while True:
											ans = input('  - 请输入需要弃置的卡牌编号({}): '.format(card_ids))
											if not ans.isdigit():
												print('    + 请按照要求正确输入!')
												continue
											ans = int(ans)
											if not ans in wxkj_ids:
												print('    + 输入编号不在手牌中!')
												continue
											break
									else:
										ans = random.sample(card_ids, 1)[0]
									# 雌雄双股剑弃牌
									for i in range(len(targets[0].hand_area)):
										if targets[0].hand_area[i].id == ans:
											deck.discards.append(targets[0].hand_area.pop(i))
								else:										 # 摸牌
									card = fetch_cards(deck=deck, number=1)[0]
									self.hand_area.append(card)
						
				if armor is not None:									 # 存在防具
					is_effect = True
					if arms is not None and arms.name == CardConfig.qgj: # 青釭剑无视防具
						is_effect = True								 # 此杀必然生效
					elif armor.name == CardConfig.bgz:					 # 八卦阵
						if mode == 'manual':
							while True:
								ans = input('是否使用八卦阵?(|是->1|否->0|)')
								if not ans.isdigit():
									print('    + 请按照要求正确输入!')
									continue
								ans = int(ans)
								if not ans in [0, 1]:
									print('    + 请按照要求正确输入!')
									continue
								break
						else:
							ans = random.randint(0, 1)
						if ans == 1:
							card = fetch_cards(deck=deck, number=1)[0]
							print('  - 判定牌: ')
							display_card(card)
							if card.suit in [CardConfig.heart, CardConfig.diamond]:
								print('  - 八卦阵判定生效: ', end='')
								is_effect = False
							elif card.suit in [CardConfig.spade, CardConfig.club]:
								print('  - 八卦阵判定失效: ', end='')
							else:
								assert False, '未知的花色!'
					elif armor.name == CardConfig.rwd:					 # 仁王盾
						if card.suit in [CardConfig.spade, CardConfig.club]:
							print('  - 黑杀无效!')
							is_effect = False
					elif armor.name == CardConfig.ratton_armor:			 # 藤甲
						if not card_name in [CardConfig.fire_kill, CardConfig.thunder_kill]:
							print('  - 普通杀无效')
							is_effect = False
				if is_effect:											 # 若此杀生效
					if mode == 'manual':
						while True:
							ans = input('是否使用闪?(|是->1|否->0|)')
							if not ans.isdigit():
								print('    + 请按照要求正确输入!')
								continue
							ans = int(ans)
							if not ans in [0, 1]:
								print('    + 请按照要求正确输入!')
								continue
							break	
					else:
						ans = random.randint(0, 1)				
					if ans == 1:										 # 出闪
						dodges = targets[0].get_all_dodges()
						dodge_ids = [dodge.id for dodge in dodges]
						for dodge in dodges:
							print('  {}: ', end='')
							display(dodge)
						if mode == 'manual':
							while True:
								ans = input('  - 请选择需要使用的闪卡牌编号({}): '.format(wxkj_ids))
								if not ans.isdigit():
									print('    + 请按照要求正确输入!')
									continue
								ans = int(ans)
								if not ans in dodge_ids:
									print('    + 输入编号不在手牌中!')
								break
						else:
							ans = random.sample(dodge_ids, 1)[0]
						for i in range(len(targets[0].hand_area)):
							if targets[0].hand_area[i].id == ans:
								deck.discards.append(targets[0].hand_area.pop(i))	
						
						
						if arms is not None:							 # 闪避可能触发的武器效果
							# 贯石斧
							if arms.name == CardConfig.gsf:
								pass
							# 青龙偃月刀
							if arms.name == CardConfig.qlyyd:
								pass

						
					else:												 # 不出闪
						if arms is not None:							 # 不出闪可能触发的武器效果
							# 麒麟弓
							if arms.name == CardConfig.qlg:
								pass
							# 寒冰剑
							if arms.name == CardConfig.hbj:
								pass
							
						if armor is not None:
							if armor.name == CardConfig.bysz:			 # 白银狮子减伤害
								hurt_point = 1
							else:
								hurt_point = 1 + self.total_liquor
						else:
							hurt_point = 1 + self.total_liquor
						# 火杀藤甲加伤害
						hurt_point += (card_name == CardConfig.fire_kill and armor.name == CardConfig.ratton_armor)
						# 古锭刀捅菊花加伤害
						hurt_point += (arms.name is not None and arms.name == CardConfig.gdd and len(targets[0].hand_area) == 0)
						hurt(deck=deck, character=targets[0], hurt_point=hurt_point)
				deck.discards.append(card.copy())
				
			elif card_name == CardConfig.dodge:							 # [闪]
				raise Exception('闪不能直接使用!')
				
			elif card_name == CardConfig.peach:							 # [桃]
				if self.health_point == self.health_point_max:
					raise Exception('未受伤不能使用桃!')
				self.health_point += 1
				deck.discards.append(card)

			elif card_name == CardConfig.liquor:						 # [酒]
				if self.total_liquor > 0:
					raise Exception('已经处于酒状态, 无法使用酒!')
				else:
					self.total_liquor += 1
					deck.discards.append(card)
			else:
				assert False, '未知的基本牌!'
			
		elif card_first_class == CardConfig.tips:						 # 使用锦囊牌
			 # 延时类锦囊
			if card_second_class == CardConfig.delay_tips:				
				if card_name == CardConfig.lbss:						 # [乐不思蜀]
					assert len(targets) == 1							 # 乐不思蜀的目标只能有一个
					assert not targets[0].id == self.id					 # 乐不思蜀不能对自己使用
					for card in targets[0].judgment_area:
						if card.name == CardConfig.lbss:
							raise Exception('角色判定区已经存在乐不思蜀!')
					targets[0].judgment_area.append(card.copy())		 # 将乐不思蜀置入其判定区
				if card_name == CardConfig.blcd:						 # [兵粮寸断]
					assert len(targets) == 1							 # 兵粮寸断的目标只能有一个
					assert not targets[0].id == self.id					 # 兵粮寸断不能对自己使用
					for card in targets[0].judgment_area:
						if card.name == CardConfig.blcd:
							raise Exception('角色判定区已经存在兵粮寸断!')
					distance = calc_distance(source_character=self, target_character=targets[0], base=1)
					if distance > 1:
						raise Exception('无法对距离为{}的角色使用兵粮寸断!'.format(distance))
					targets[0].judgment_area.append(card.copy())		 # 将兵粮寸断置入其判定区
				if card_name == CardConfig.lightning:					 # [闪电]
					for card in self.judgement_area:
						if card.name == CardConfig.lightning:
							raise Exception('角色判定区已经存在闪电')
					
			# 非延时类锦囊
			elif card_second_class == CardConfig.immediate_tips:		 
				if card_name == CardConfig.ghcq:						 # [过河拆桥]
					pass
				if card_name == CardConfig.ssqy:						 # [顺手牵羊]
					pass
				if card_name == CardConfig.wzsy:						 # [无中生有]
					pass
				if card_name == CardConfig.tslh:						 # [铁索连环]
					assert len(targets) < 3, '铁索连环的目标数应当小于3!'
					if len(targets) == 0:								 # 铁索连环目标数为0指重铸
						new_card = recast_card(deck=deck, card=card)
						self.hand_area.append(new_card)
					else:												 # 铁索连环目标数为1或2时即为使用
						for target in targets:
							target.is_bound = not target.is_bound		 # 铁索连环的效果是使处于横置状态的角色重置, 或使处于重置状态的角色横置
					
				elif card_name == CardConfig.wxkj:						 # [无懈可击]
					raise Exception('无懈可击不能直接使用!')
				elif card_name == CardConfig.nmrq:						 # [南蛮入侵]
					pass
				elif card_name == CardConfig.wjqf:						 # [万箭齐发]
					pass
				elif card_name == CardConfig.tyjy:						 # [桃园结义]
					pass
				elif card_name == CardConfig.wgfd:						 # [五谷丰登]
					pass
				elif card_name == CardConfig.jdsr:						 # [借刀杀人]
					pass
				elif card_name == CardConfig.duel:						 # [决斗]
					pass
				elif card_name == CardConfig.fire_attack:				 # [火攻]
					pass
				deck.discards.append(card)								 # 所有非延时类锦囊使用后都会置入弃牌堆
			else:
				assert False, '未知的锦囊牌二级分类!'	
		elif card_first_class == CardConfig.equipment:					 # 使用装备牌: 此时target可以为None, 因为必须为自己
			current_equipment = self.equipment_area[card_second_class]	 # 找到当前装备区中的卡牌
			self.equipment_area[card_second_class] = card				 # 将装备牌置入该区域
			if current_equipment is not None:							 # 如果需要使用的装备牌对应的装备区已经有卡牌
				deck.discards.append(current_equipment)					 # 将这张卡牌置入弃牌堆
				
			if card_second_class == CardConfig.attack_horse:
				game == kwargs.get('game')
				game.
		else: 
			assert False, '未知的卡牌一级分类'
	

if __name__ == '__main__':
	
	pass

example_character.py

# -*- coding: UTF-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

from example_utils import *
from example_deck import Deck
from example_base import BaseCharacter


class SunQuan(BaseCharacter):
	"""孙权"""
	def __init__(self, id: int=0, health_point: int=4, health_point_max: int=4, is_male: bool=True) -> None:
		BaseCharacter.__init__(self,
			id=id,
			health_point=health_point,
			health_point_max=health_point_max,
			is_male=is_male,
		)

	def play_phase(self, game):
		"""出牌阶段"""
		BaseCharacter.play_phase(self, game)
		
	def zhiheng(self, deck: Deck, discards: list, is_new: bool=True) -> None:
		"""制衡"""
		handcard_count = 0												 # 统计制衡弃置多少张手牌
		# 1 检验制衡弃牌在手牌区及装备区可以找到
		for discard in discards:
			in_area = False												 # 判断其是否在孙权区域中的标识
			for card in self.equipment_area.values():					 # 遍历孙权装备区的卡牌
				if discard.id == card.id:								 # 以卡牌编号作为唯一标识
					in_area = True
					break
			if in_area:
				continue
			for card in self.hand_area:									 # 遍历孙权手牌区的卡牌					
				if discard.id == card.id:								 # 以卡牌编号作为唯一标识
					in_area = True
					handcard_count += 1									 # 记录制衡弃置手牌的数量: 便于实现界制衡的额外效果
					break
			assert in_area, '需要制衡的卡牌不在孙权的手牌区或装备区'			 # 找不到需要弃置的卡牌则抛出异常
		
		# 2 确定制衡获得的卡牌
		number = len(discards)
		if is_new and handcard_count == len(self.hand_area):			 # 界制衡额外效果实现条件
			number += 1													 # 界制衡额外获得一张牌
		new_cards = fetch_cards(deck=deck, number=number)				 # 从牌堆摸制衡得到的卡牌
		
		# 3 将制衡弃牌从孙权的区域中置入弃牌堆
		deck.discards.extend(discards)
		

class WangYi(BaseCharacter):
	"""王异"""
	def __init__(self, id: int=0, health_point: int=3, health_point_max: int=3, is_male: bool=False) -> None:
		BaseCharacter.__init__(self, 
			id=id,
			health_point=health_point,
			health_point_max=health_point_max,
			is_male=is_male,
		)

	def play_phase(self, game):
		"""出牌阶段"""
		BaseCharacter.play_phase(self, game)
		
	def ending_phase(self, deck: Deck, is_skill: bool=True): 
		"""结束阶段"""
		if is_skill:													 # 结束阶段默认发动秘技
			self.miji(deck)
			
	def miji(self, deck):
		"""秘技"""
		if self.health_point < self.health_point_max:					 # 秘技发动条件: 已受伤
			cards = fetch_cards(deck, self.health_point_max - self.health_point)
			self.hand_area.extend(cards)								 # 秘技摸牌加入到手牌中
		else:
			print('不满足秘技发动条件')
		


if __name__ == '__main__':
	sq = SunQuan()
	pass

example_config.py

# -*- coding: UTF-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

class DeckConfig:	
	default_deck_filepath = 'default_deck.txt'

class CardConfig:	
	# 1 卡牌名称
	# 1.1 基本牌
	kill			= '杀'
	fire_kill		= '火杀'
	thunder_kill	= '雷杀'
	dodge			= '闪'
	peach			= '桃'
	liquor			= '酒'
	# 1.2 锦囊牌
	# 1.2.1 非延时类锦囊牌
	ghcq			= '过河拆桥'
	ssqy			= '顺手牵羊'
	wzsy			= '无中生有'
	tslh			= '铁索连环'
	wxkj			= '无懈可击'
	nmrq			= '南蛮入侵'
	wjqf			= '万箭齐发'
	tyjy			= '桃园结义'
	wgfd			= '五谷丰登'
	jdsr			= '借刀杀人'
	duel			= '决斗'
	fire_attack		= '火攻'
	# 1.2.2 延时类锦囊牌
	lbss			= '乐不思蜀'
	blcd			= '兵粮寸断'
	lightning		= '闪电'
	# 1.3 装备牌
	# 1.3.1 装备牌: 武器
	zgln			= '诸葛连弩'
	qgj				= '青釭剑'
	cxsgj			= '雌雄双股剑'
	hbj				= '寒冰剑'
	gdd				= '古锭刀'
	zbsm			= '丈八蛇矛'
	qlyyd			= '青龙偃月刀'
	gsf				= '贯石斧'
	zqys			= '朱雀羽扇'
	fthj			= '方天画戟'
	qlg				= '麒麟弓'
	# 1.3.2 装备牌: 防具
	bgz				= '八卦阵'
	ratton_armor	= '藤甲'
	rwd				= '仁王盾'
	bysz			= '白银狮子'
	# 1.3.3 装备牌: 进攻马
	chitu			= '赤兔'
	dawan			= '大宛'
	zixing			= '紫骍'
	# 1.3.4 装备牌: 防御马
	dilu			= '的卢'
	zhfd			= '爪黄飞电'
	jueying			= '绝影'
	hualiu			= '骅骝'
	# 1.3.5 装备牌: 宝物
	mnlm			= '木牛流马'
	
	# 2 卡牌类别
	# 2.1 一级分类
	basic			= '基本牌'
	tips			= '锦囊牌'
	equipment		= '装备牌'
	# 2.2 二级分类
	immediate_tips	= '非延时类锦囊'
	delay_tips		= '延时类锦囊'
	arms			= '武器'
	armor			= '防具'
	defend_horse	= '防御马'
	attack_horse	= '进攻马'
	treasure		= '宝物'

	# 3 卡牌花色
	spade			= '黑桃'
	heart			= '红心'
	club			= '梅花'
	diamond			= '方片'

	# 4 卡牌区域
	hand_area		= '手牌区'
	equipment_area	= '装备区'
	judgment_area	= '判定区'
	deck_area		= '牌堆区'
	settlement_area	= '结算区'
	discard_area	= '弃牌区'
	mnlm_area		= '木牛流马区'
	
	# 5 其他配置信息
	name2attr = {
		kill: 			[basic, basic, None],
		fire_kill: 		[basic, basic, None],
		thunder_kill: 	[basic, basic, None],
		dodge: 			[basic, basic, None],
		peach: 			[basic, basic, None],
		liquor: 		[basic, basic, None],
		ghcq: 			[tips, immediate_tips, None],
		ssqy: 			[tips, immediate_tips, None],
		wzsy: 			[tips, immediate_tips, None],
		tslh: 			[tips, immediate_tips, None],
		wxkj: 			[tips, immediate_tips, None],
		nmrq: 			[tips, immediate_tips, None],
		wjqf: 			[tips, immediate_tips, None],
		tyjy: 			[tips, immediate_tips, None],
		wgfd: 			[tips, immediate_tips, None],
		jdsr: 			[tips, immediate_tips, None],
		duel: 			[tips, immediate_tips, None],
		fire_attack: 	[tips, immediate_tips, None],
		lbss: 			[tips, delay_tips, None],
		blcd: 			[tips, delay_tips, None],
		lightning: 		[tips, delay_tips, None],
		zgln: 			[equipment, arms, 1],
		qgj: 			[equipment, arms, 2],
		cxsgj: 			[equipment, arms, 2],
		hbj: 			[equipment, arms, 2],
		gdd: 			[equipment, arms, 2],
		zbsm: 			[equipment, arms, 3],
		qlyyd: 			[equipment, arms, 3],
		gsf: 			[equipment, arms, 3],
		zqys: 			[equipment, arms, 4],
		fthj: 			[equipment, arms, 4],
		qlg: 			[equipment, arms, 5],
		bgz: 			[equipment, armor, None],
		ratton_armor: 	[equipment, armor, None],
		rwd: 			[equipment, armor, None],
		bysz: 			[equipment, armor, None],
		chitu: 			[equipment, attack_horse, None],
		dawan: 			[equipment, attack_horse, None],
		zixing: 		[equipment, attack_horse, None],
		dilu: 			[equipment, defend_horse, None],
		zhfd: 			[equipment, defend_horse, None],
		jueying: 		[equipment, defend_horse, None],
		hualiu: 		[equipment, defend_horse, None],
		mnlm: 			[equipment, treasure, None],
	}

example_deck.py

# -*- coding: UTF-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

import random

from example_config import CardConfig, DeckConfig
from example_base import BaseCard
from example_utils import *

class Deck(object):
	def __init__(self, cards: list=None, discards: list=None) -> None:
		if cards is None:												 # 没有给定
			self.load_default_deck()
		else:															 # 否则直接使用给定的卡牌创建新牌堆
			self.cards = cards
		# random.shuffle(self.cards)										 # 洗牌
		self.discards = [] if discards is None else discards			 # 初始化弃牌堆
		self.id2card = {card.id: card for card in self.cards}			 # 构建一个卡牌的索引, 便于查询
		
	def load_default_deck(self) -> None:
		"""默认军争牌堆(带木牛流马, 161张牌)"""
		self.cards = []
		cardconfig_dict = get_config_dict(CardConfig)
		index = -1
		with open(DeckConfig.default_deck_filepath, 'r', encoding='utf8') as f:
			lines = f.read().splitlines()
		# default_deck_filepath配置文件的数据处理
		for line in filter(None, lines):								 # 注意删除配置文件中的空行
			entry = line.strip()
			if entry.startswith('[') and entry.endswith(']'):			 # 花色名称包含在'['与']'之间
				suit = CardConfig.__getattribute__(CardConfig, cardconfig_dict.get(entry[1:-1]))
			else:														 # 其他每行都是点数与花色相同的数张卡牌名称
				cells = entry.split('|')								 # 每行的数据使用'|'分隔
				point = cells[0]										 # 点数位于行首
				for cell in cells[1:]:									 # 其他位置都是卡牌名称
					name = CardConfig.__getattribute__(CardConfig, cardconfig_dict.get(cell))
					first_class, second_class, attack_range = CardConfig.name2attr.get(name)
					index += 1
					kwargs = {											 # 新卡牌的参数
						'id': index,
						'name': name,
						'first_class': first_class,
						'second_class': second_class,
						'suit': suit,
						'point': point,
						'attack_range': attack_range,
					}
					card = BaseCard(**kwargs)							 # 创建新卡牌
					self.cards.append(card)								 # 将新卡牌添加到牌堆中
	
	def refresh(self) -> None:
		"""更新牌堆"""
		random.shuffle(self.discards)									 # 弃牌堆洗牌
		self.cards.extend(self.discards)								 # 将洗好的弃牌堆卡牌添加到牌堆下面
		self.discards = []												 # 置空弃牌堆
	
if __name__ == '__main__':
	
	deck = Deck()
	index = 0
	for card in deck.cards:
		index += 1
		print(index, card.suit, card.point, card.name, card.first_class, card.second_class, card.area, card.attack_range)

example_game.py

# -*- coding: UTF-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

import random
from example_utils import *
from example_deck import Deck
from example_base import BaseCharacter
from example_config import CardConfig
from example_character import *

class SoloGame(object):
	
	def __init__(self, player1: BaseCharacter, player2: BaseCharacter) -> None:
		self.deck = Deck(cards=None)									 # 初始化牌堆
		self.player1 = player1											 # 先手玩家
		self.player2 = player2 											 # 后手玩家
		self.player1.hand_area.extend(fetch_cards(deck=self.deck ,number=4)) # 先手玩家初始手牌
		self.player2.hand_area.extend(fetch_cards(deck=self.deck ,number=4)) # 后手玩家初始手牌
		
	def run(self) -> None:
		round_count = 0
		while True:
			round_count += 1
			print('===== 第{}轮开始 ====='.format(round_count))
			print('===== player1 回合开始 =====')
			player1_draw_flag = True
			player1_play_flag = True
			self.player1.round_start_phase(deck=self.deck)
			print('===== player1 准备阶段 =====')
			self.player1.preparation_phase(deck=self.deck)
			print('===== player1 判定阶段 =====')
			judgment_result = self.player1.judgment_phase(game=self)
			for key, value in judgment_result.items():
				print('  - {}: '.format(key), end='')
				display_card(value)
				if value is None:
					continue
				if key == CardConfig.lbss:
					if not value.suit == CardConfig.heart:
						player1_play_flag = False
				elif key == CardConfig.blcd:
					if not value.suit == CardConfig.club:
						player1_draw_flag = False			
				elif key == CardConfig.lightning:
					if value.suit == CardConfig.spade and value.point <= 9 and value.point >= 2:
						hurt(deck=self.deck, character=self.player1, hurt_point=3)
				else:
					assert False, '未知的延时类锦囊'		
			print('===== player1 摸牌阶段 =====')
			if player1_draw_flag:
				self.player1.draw_phase(deck=self.deck, number=2)
			else:
				print('  - 跳过!')
			print('===== player1 出牌阶段 =====')
			if player1_play_flag:
				self.player1.play_phase(game=self)
			else:
				print('  - 跳过!')
			print('===== player1 弃牌阶段 =====')
			self.player1.fold_phase(deck=self.deck)
			print('===== player1 结束阶段 =====')
			self.player1.ending_phase(deck=self.deck)
			print('===== player1 回合结束 =====')
			self.player1.round_end_phase(deck=self.deck)
			self.player1.round_start_phase(deck=self.deck)
			print('  - player1: ', len(self.deck.cards), len(self.deck.discards))
			
			############################################################
			
			print('===== player2 回合开始 =====')
			player2_draw_flag = True
			player2_play_flag = True
			self.player2.round_start_phase(deck=self.deck)
			print('===== player2 准备阶段 =====')
			self.player2.preparation_phase(deck=self.deck)
			print('===== player2 判定阶段 =====')
			judgment_result = self.player2.judgment_phase(game=self)
			for key, value in judgment_result.items():
				if value is None:
					continue
				if key == CardConfig.lbss:
					if not value.suit == CardConfig.heart:
						player2_play_flag = False
				elif key == CardConfig.blcd:
					if not value.suit == CardConfig.club:
						player2_draw_flag = False			
				elif key == CardConfig.lightning:
					if value.suit == CardConfig.spade and value.point <= 9 and value.point >= 2:
						hurt(deck=self.deck, character=self.player2, hurt_point=3)
				else:
					assert False, '未知的延时类锦囊'	
			print('===== player2 摸牌阶段 =====')
			if player1_draw_flag:
				self.player2.draw_phase(deck=self.deck, number=2)
			print('===== player2 出牌阶段 =====')
			if player1_play_flag:
				self.player2.play_phase(game=self)
			print('===== player2 弃牌阶段 =====')
			self.player2.fold_phase(deck=self.deck)
			print('===== player2 结束阶段 =====')
			self.player2.ending_phase(deck=self.deck)
			print('===== player2 回合结束 =====')
			self.player2.round_end_phase(deck=self.deck)
			self.player1.round_start_phase(deck=self.deck)
			print('  - player2: ', len(self.deck.cards), len(self.deck.discards))

	def display_game(self, max_display=4) -> None:
		print('#' * 64)
		print('牌堆剩余{}张'.format(len(self.deck.cards)))
		for card in self.deck.cards[:max_display]:
			print(' - ', end='')
			display_card(card)
		print('-' * 64)
		print('弃牌堆中{}张'.format(len(self.deck.discards)))
		for card in self.deck.discards[:max_display]:
			print(' - ', end='')
			display_card(card)
		print('-' * 64)
		print('player1: ')
		display_character(self.player1)
		print('-' * 64)
		print('player2: ')
		display_character(self.player2)
		print('-' * 64)	
		
	def wxkj_polling(self, card, mode: str='manual') -> bool:
		"""
		无懈可击轮询
		:param card: 无懈可击所针对的锦囊牌;
		:param mode: 操作模式, 目前只写了手动模式(manual);
		:return is_effect: 锦囊牌是否生效
		"""		
		settlement_area = []											 # 临时结算区
		current_card = card.copy()										 # 当前无懈可击针对的卡牌
		
		while True:
			display_card(current_card)
			print('  - 对上述卡牌生效的无懈可击的轮询开始...')
			end_flag = True												 # 结算终止标识
			for player in [self.player1, self.player2]:					 # 使用轮询模式
				if player.is_wxkj_exist:								 # 如果当前玩家手牌中有无懈可击
					if mode == 'manual':
						while True:
							ans = input('  - 是否需要使用无懈可击?(|1->是|0->否|)')
							if not ans.isdigit():
								print('    + 请按照要求正确输入!')
								continue
							ans = int(ans)
							if not ans in [0, 1]:
								print('    + 请按照要求正确输入!')
								continue
							break
					else:
						ans = random.randint(0, 1)
					if ans == 1:										 # 使用无懈可击
						wxkjs = player.get_all_wxkjs()
						wxkj_ids = [wxkj.id for wxkj in wxkjs]
						for wxkj in wxkjs:
							display_card(wxkj)
						if mode == 'manual':
							while True:
								ans = input('  - 请选择需要使用的无懈可击卡牌编号({}): '.format(wxkj_ids))
								if not ans.isdigit():
									print('    + 请按照要求正确输入!')
									continue
								ans = int(ans)
								if not ans in wxkj_ids:
									print('    + 输入编号不在手牌中!')
								break
						else:
							ans = random.sample(wxkj_ids, 1)[0]
						# 打出手牌中的无懈可击并放入结算区
						for i in range(len(player.hand_area)):
							if player.hand_area[i].id == ans:
								settlement_area.append(player.hand_area.pop(i))
								break
						end_flag = False								 # 有人打出无懈可击则继续结算
						current_card = settlement_area[-1].copy()		 # 结算对象为结算区的最后一张牌
					else:												 # 不使用无懈可击
						continue
				else:													 # 没有无懈可击可以使用
					continue
			if end_flag: 												 # 结算终止
				break
		self.deck.discards.extend(settlement_area)						 # 将结算区所有卡牌置入弃牌堆
		self.deck.discards.append(card)									 # 将锦囊牌放入弃牌堆
		if len(settlement_area) % 2:									 # 结算区有奇数张无懈可击则锦囊牌失效
			return False
		return True														 # 否则锦囊牌生效


if __name__ == '__main__':
	sunquan = SunQuan(id=0, health_point=4, health_point_max=4, is_male=True)
	wangyi = WangYi(id=1, health_point=4, health_point_max=4, is_male=False)
	game = SoloGame(player1=sunquan, player2=wangyi)
	game.run()

example_utils.py

# -*- coding: UTF-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

from example_config import CardConfig

def get_config_dict(config: type) -> dict:
	"""获取配置类的参数字典(键值反转)"""
	config_dict = {}
	for key in dir(config):
		if key.startswith('__') and key.endswith('__'):
			continue
		try: 
			config_dict[config.__getattribute__(config, key)] = key
		except:
			 continue
	return config_dict

def display_card(card) -> None:
	if card is None:
		print(card)
	else:
		print(card.id, card.suit, card.point, card.name)

def display_character(character) -> None:
	print('  - 体力值: {}'.format(character.health_point))
	print('  - 体力上限: {}'.format(character.health_point_max))
	print('  - 手牌区: {}张'.format(len(character.hand_area)))
	for card in character.hand_area:
		print('    + ', end='')
		display_card(card)
	equipment_count = 0
	for card in character.equipment_area.values():
		if card is not None:
			equipment_count += 1
	print('  - 装备区: {}张'.format(equipment_count))
	for area, card in character.equipment_area.items():
		print('    + {}: '.format(area), end='')
		display_card(card)
	print('  - 判定区: {}张'.format(len(character.judgment_area)))
	for card in character.judgment_area:
		print('    + ', end='')
		display_card(card)

def fetch_cards(deck, number: int=1, from_top=True) -> list:
	"""从牌堆获取若干张卡牌"""
	if len(deck.cards) < number:
		print('  - 触发洗牌')
		deck.refresh()
		if len(deck.cards) < number:
			raise Exception('平局')
	cards = [deck.cards.pop(0 if from_top else -1) for _ in range(number)]
	return cards
	
def recast_card(deck, card):
	"""重铸卡牌"""
	new_card = fetch_cards(deck=deck, number=1)[0]
	deck.discards.append(card)
	return new_card

def hurt(deck, character, hurt_point: int=1):
	"""受到伤害"""
	armor = character.equipment_area[CardConfig.armor]
	if armor is not None and armor.name == CardConfig.bysz:				 # 白银狮子减伤
		hurt_point = 1
	character.health_point -= hurt_point
	if character.health_point <= 0:
		character.ask_for_peach(deck, mode='manual')
		
def calc_distance(source_character, target_character, base: int=1) -> int:
	"""计算距离"""
	return max(1, base - (source_character.equipment_area[CardConfig.attack_horse] is not None) + (target_character.equipment_area[CardConfig.defend_horse] is not None))

def calc_attack_range(character):
	"""计算攻击范围"""
	if character.equipment_area[CardConfig.arms] is None:
		return 1
	return character.equipment_area[CardConfig.arms].attack_range


if __name__ == '__main__':
	
	pass

更新日志

  1. 2021-01-10:init commit,未完成的烂活。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页