待办三国杀单挑测试脚本
Posted 囚生CY
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了待办三国杀单挑测试脚本相关的知识,希望对你有一定的参考价值。
序言
前天晚上开始写之前一直想写的三国杀单挑仿真项目,初始灵感是想要测试四血界孙权单挑四血新王异在不同状态下的优劣(如新王异零装备起手,单+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) == 洛谷 P2128 赤壁之战