种子求解器
这是什么?
这是一个暴力猜解词条种子的脚本工具。
这有什么用?
如果你错过了开服的种子公开展示的时机,也可以通过这个工具尝试重新找回种子。
该如何使用?
安装Python3,推荐3.12,是开发时使用的环境。
安装pysat库。
pip install python-sat
如果在国内出现超时,又不会用VPN的话,可以试试加-i使用镜像源,例如:
pip install python-sat -i https://pypi.tuna.tsinghua.edu.cn/simple
其它镜像源: 中国科学技术大学 : https://pypi.mirrors.ustc.edu.cn/simple/ 豆瓣:http://pypi.douban.com/simple/ 阿里云:http://mirrors.aliyun.com/pypi/simple/
复制本文下方的python脚本,保存到 HBRGuess.py文件。
复制本文下方的 lottery_data.json,保存到 HBRGuess.py文件同目录,记得使用GBK编码保存。
在 HBRGuess.py文件同目录创建一个名为 config.json的文件,复制下面内容到 config.json中:
{ "start_index": 1, "search_depth": 20, "search_length": 1000, "requires": [ ] }
阅读上一篇词条计算器说明,可以知道计算词条需要两个必要的值,index和seed。index可以通过已经制作的装备来估算,而seed就是本工具求解的值。index可以通过项链+2,手环+4,耳环+3,粗略估算一下当前index的范围,越精细后续需要计算的越少。
如果估算当前index大概在300-400之间,那么将 config.json中的 start_index改为300,search_length改为100.其它范围以此类推。
连续打造一组装备/连续开孔/连续铣孔,获得一组连续的词条,注意,一定要保持连续!将连续的词条按照顺序填入 requires中。
词条的名称不是按照游戏中的名称来,需要填入 lottery_data.json中 Description的值,比如”灵巧 +2”、”攻击属性变化”。
填写完后的示例,记得使用GBK编码保存:
{ "start_index": 450, "search_depth": 20, "search_length": 100, "requires": [ "幸运 +2", "灵巧 +2", "DP +30", "灵巧 +2", "力量 +1", "HP +30", "灵巧 +2", "HP +20", "体力 +2", "灵巧 +3", "灵巧 +1", "HP +10", "DP +30", "幸运 +1", "力量 +1", "幸运 +1" ] }
打开控制台,在py脚本目录执行,即可开始计算。
python HBRGuess.py
如果因为意外计算中断,可以使用以下指令继续上次的计算:
python HBRGuess.py use_cache
计算结果除了实时打印,还可以在 HBRGuess.py文件同目录下的 cache.json文件找到,其中键值为index,值为seed。
注意事项
关于脚本
本脚本完全开源,欢迎大家进行改造优化,毕竟python嘛,性能差是预期中的。
填多少词条比较好?
体感装备打造10个以上,打孔/洗词条15个以上,可以比较准确地算出seed。
为什么有的时候可以算出多个seed?
这里的seed只是可能符合你的数据,你可以逐个试试,不能保证结果一定正确。
config.json里的search_depth是什么?
这个是求解工具中的一个估算程度设置,1-32,数值越高计算越详细,但耗时也会增加,可以自己改改找找最快的配置。
洗词条为什么有时候算不出来?
HBR洗词条有一个隐藏逻辑,随机生成词条的时候,如果新词条和当前词条一样,会跳过继续生成下一条。
如果运气比较差,出现了这种情况,估计就只能重新生成一些词条再计算了,毕竟是否发生这个情况是无法感知的。
脚本和数据
HBRGuess.py 文件
import math
import datetime
import json
import sys
from pysat.formula import *
from pysat.solvers import Solver
# 常量推演
class BaseTransform:
def __init__(self):
self.box = [
0b00000111010110111100110100010101,
0b00010101100110100101010111100101,
0b00011111000100100011101110110101,
0b00000000000000000000000000000000,
]
def shift(self):
tmp = (self.box[0] ^ (self.box[0] << 11)) & 0xFFFFFFFF
self.box[0] = self.box[1]
self.box[1] = self.box[2]
self.box[2] = self.box[3]
self.box[3] = tmp ^ (tmp >> 8) ^ self.box[3] ^ (self.box[3] >> 19) & 0xFFFFFFFF
def get_base(self):
return self.box[3]
def deepcopy(self):
new_self = BaseTransform()
new_self.box[0] = self.box[0]
new_self.box[1] = self.box[1]
new_self.box[2] = self.box[2]
new_self.box[3] = self.box[3]
return new_self
# seed推演
class NormalFormTransform:
def __init__(self):
self.bit = [
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
],
[
1 << 0,
1 << 1,
1 << 2,
1 << 3,
1 << 4,
1 << 5,
1 << 6,
1 << 7,
1 << 8,
1 << 9,
1 << 10,
1 << 11,
1 << 12,
1 << 13,
1 << 14,
1 << 15,
1 << 16,
1 << 17,
1 << 18,
1 << 19,
1 << 20,
1 << 21,
1 << 22,
1 << 23,
1 << 24,
1 << 25,
1 << 26,
1 << 27,
1 << 28,
1 << 29,
1 << 30,
1 << 31,
],
]
def shift(self):
# tmp = (self.box[0] ^ (self.box[0] << 11)) & 0xFFFFFFFF
tmp = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]
for i in range(0, 11):
tmp[i] = self.bit[0][i]
for i in range(11, 32):
tmp[i] = self.bit[0][i] ^ self.bit[0][i - 11]
# self.box[0] = self.box[1]
for i in range(0, 32):
self.bit[0][i] = self.bit[1][i]
# self.box[1] = self.box[2]
for i in range(0, 32):
self.bit[1][i] = self.bit[2][i]
# self.box[2] = self.box[3]
for i in range(0, 32):
self.bit[2][i] = self.bit[3][i]
# self.box[3] = tmp ^ (tmp >> 8) ^ self.box[3] ^ (self.box[3] >> 19) & 0xFFFFFFFF
tmp2 = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]
for i in range(24, 32):
tmp2[i] = tmp[i]
for i in range(0, 24):
tmp2[i] = tmp[i] ^ tmp[i + 8]
tmp3 = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]
for i in range(0, 13):
tmp3[i] = tmp2[i] ^ self.bit[3][i] ^ self.bit[3][i + 19]
for i in range(13, 32):
tmp3[i] = tmp2[i] ^ self.bit[3][i]
# return self.box[3]
for i in range(0, 32):
self.bit[3][i] = tmp3[i]
def transform_seed(self, seed):
result = 0
for i in range(0, 32):
mask = seed & self.bit[3][i]
mask = mask ^ (mask >> 1)
mask = mask ^ (mask >> 2)
mask = mask ^ (mask >> 4)
mask = mask ^ (mask >> 8)
mask = mask ^ (mask >> 16)
if (mask & 1) != 0:
result += 1 << i
return result
def get_form(self):
return self.bit[3]
def deepcopy(self):
new_self = NormalFormTransform()
for i in range(0, 32):
new_self.bit[0][i] = self.bit[0][i]
new_self.bit[1][i] = self.bit[1][i]
new_self.bit[2][i] = self.bit[2][i]
new_self.bit[3][i] = self.bit[3][i]
return new_self
# 范式构造
class CNFFactory:
def __init__(self, start_index, ranges):
self.base = BaseTransform()
self.form = NormalFormTransform()
self.index = 0
self.requires = []
for i in range(0, start_index):
self.shift()
self.index = start_index
for real_range in ranges:
interval = math.floor(0xFFFFFFFE / (real_range[1] - real_range[0])) + 1
real_min = (real_range[2] - real_range[0]) * interval + 1
real_max = (real_range[3] - real_range[0]) * interval + 1
self.requires.append([real_min, min(real_max, 0xFFFFFFFF)])
def shift(self):
self.base.shift()
self.form.shift()
def create_cnf(self, search_depth=32):
seed = []
search_depth = max(1, search_depth)
search_depth = min(32, search_depth)
for i in range(0, 32):
v_name = "seed$" + str(i)
v_a = Atom(v_name)
v_a._clausify()
seed.append(v_a)
tmp_base = self.base.deepcopy()
tmp_form = self.form.deepcopy()
const_false = Atom("const_false")
const_true = Atom("const_true")
formulas = []
formulas.append(Neg(const_false))
formulas.append(const_true)
for require in self.requires:
# 向后推演
tmp_base.shift()
tmp_form.shift()
# 无用条件,跳过
if require[0] == 1 and require[1] == 0xFFFFFFFF:
continue
# 构建seed变换范式
trans_seed = []
for i in range(0, 32):
tmp_bit = []
for bit_i in range(0, 32):
bit_flag = (tmp_form.get_form()[i] >> bit_i) & 1 == 1
if bit_flag:
tmp_bit.append(seed[bit_i])
bit_formulas = []
for bit_i in range(0, len(tmp_bit)):
bit_formulas.append(tmp_bit[bit_i])
trans_seed.append(XOr(*bit_formulas, const_false, const_false))
# 构建位头部相等范式
bit_term_index = 31
for i in range(32, 0, -1):
tmp_bit_term_index = i - 1
if ((require[0] >> tmp_bit_term_index) & 1) != (
(require[1] >> tmp_bit_term_index) & 1
):
bit_term_index = tmp_bit_term_index
break
rand_bit = (tmp_base.get_base() >> tmp_bit_term_index) & 1
range_min_bit = (require[0] >> tmp_bit_term_index) & 1
formulas.append(
Equals(
const_true if rand_bit ^ range_min_bit == 1 else const_false,
trans_seed[tmp_bit_term_index],
)
)
# 构建位尾部不等范式
xor_bits_term = [None] * 32
xor_bits_less = [None] * 32
xor_bits_greater = [None] * 32
for ni in range(bit_term_index + 1, 32 - search_depth, -1):
i = ni - 1
rand_bit = (tmp_base.get_base() >> i) & 1
xor_bits_term[i] = XOr(
const_true if rand_bit == 1 else const_false, trans_seed[i]
)
xor_bits_less[i] = XOr(
const_true if ((require[0] >> i) & 1) == 1 else const_false,
xor_bits_term[i],
)
xor_bits_greater[i] = XOr(
const_true if ((require[1] >> i) & 1) == 1 else const_false,
xor_bits_term[i],
)
bit_less_formulas = []
bit_greater_formulas = []
for ni in range(bit_term_index + 1, 32 - search_depth, -1):
i = ni - 1
bit_less_formulas.append(
XOr(
*xor_bits_less[i : bit_term_index + 1], const_false, const_false
)
)
bit_less_formulas.append(
Neg(const_true if ((require[0] >> i) & 1) == 1 else const_false)
)
bit_less_formulas.append(xor_bits_term[i])
bit_greater_formulas.append(
XOr(
*xor_bits_greater[i : bit_term_index + 1],
const_false,
const_false
)
)
bit_greater_formulas.append(Neg(xor_bits_term[i]))
bit_greater_formulas.append(
const_true if ((require[1] >> i) & 1) == 1 else const_false
)
formulas.append(Or(*bit_less_formulas, const_false, const_false))
formulas.append(Or(*bit_greater_formulas, const_false, const_false))
formula = And(*formulas)
return formula
class SolverManager:
solver_name = "cadical153"
def __init__(
self,
config_path="config.json",
cache_path="cache.json",
lottery_data_path="lottery_data.json",
use_cache=False,
):
# 加载配置文件
self.config_path = config_path
self.cache_path = cache_path
self.lottery_data_path = lottery_data_path
with open(self.config_path, "r", encoding="gbk") as config_file:
self.config_data = json.load(config_file)
if use_cache:
with open(self.cache_path, "r", encoding="gbk") as cache_file:
self.cache_data = json.load(cache_file)
else:
self.cache_data = json.loads('{"result":{}}')
with open(self.lottery_data_path, "r", encoding="gbk") as lottery_data_file:
self.lottery_data = json.load(lottery_data_file)
assert "start_index" in self.config_data
assert "requires" in self.config_data
assert "search_depth" in self.config_data
assert "search_length" in self.config_data
assert "Labels" in self.lottery_data
assert "result" in self.cache_data
# 预处理条件数据
self.requires = []
for r in self.config_data["requires"]:
self.requires.append(self.get_require_from(r))
# 不要吐槽为什么这里计算这么绕,我只是还原原逻辑
def get_require_from(self, require_name):
require = None
for data in self.lottery_data["Labels"]:
if data["Description"] == require_name:
require = data
break
if require == None:
return [0, 100, 0, 100]
group_ratio = 0
for data in self.lottery_data["Labels"]:
if data["GroupLabel"] == require["GroupLabel"]:
group_ratio += data["Ratio"]
before_ratio = 0
for data in self.lottery_data["Labels"]:
if data["Description"] == require_name:
break
if data["GroupLabel"] == require["GroupLabel"]:
before_ratio += data["Ratio"]
return [
0,
group_ratio,
before_ratio,
before_ratio + require["Ratio"],
]
# 写出结果文件
def flush(self):
with open(self.cache_path, "w", encoding="gbk") as cache_file:
json.dump(self.cache_data, cache_file)
cache_file.close()
def solve(self, index):
if str(index) not in self.cache_data["result"]:
self.cache_data["result"][str(index)] = []
else:
return self.cache_data["result"][str(index)]
factory = CNFFactory(index, self.requires)
formulas = factory.create_cnf(self.config_data["search_depth"])
cnf = CNF()
for clause in formulas:
cnf.append(clause)
# cnf.to_file("test_out.cnf")
with Solver(name=self.solver_name, bootstrap_with=cnf) as solver:
while solver.solve():
model = solver.get_model()
seed = 0
for i in range(0, 32):
if model[i] > 0:
seed += 1 << i
print("seed:" + str(seed))
self.cache_data["result"][str(index)].append(seed)
solver.add_clause([-l for l in model[0:32]])
return self.cache_data["result"][str(index)]
def run(self):
for i in range(
self.config_data["start_index"],
self.config_data["start_index"] + self.config_data["search_length"],
):
starttime = datetime.datetime.now()
print("index:" + str(self.solve(i)))
self.flush()
endtime = datetime.datetime.now()
print(
str(i)
+ " use times {0:.2f}s".format((endtime - starttime).total_seconds())
)
def main(argv):
use_cache = len(argv) >= 2 and argv[1] == "use_cache"
manager = SolverManager(use_cache=use_cache)
manager.run()
if __name__ == "__main__":
main(sys.argv)
lottery_data.json文件
{
"Labels": [
{
"GroupLabel": "Earring.001",
"Description": "第一词条+10%",
"Ratio": 100
},
{
"GroupLabel": "Earring.001",
"Description": "第一词条+12%",
"Ratio": 100
},
{
"GroupLabel": "Earring.001",
"Description": "第一词条+15%",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +400",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +450",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +500",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +550",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +600",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +700",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +800",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +900",
"Ratio": 100
},
{
"GroupLabel": "Earring.002",
"Description": "DP +1200",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +32",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +33",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +34",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +35",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +37",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +39",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +41",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +43",
"Ratio": 100
},
{
"GroupLabel": "Earring.003",
"Description": "智慧 +48",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.001",
"Description": "通常攻击攻击力+100%",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.001",
"Description": "通常攻击攻击力+150%",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.001",
"Description": "通常攻击攻击力+200%",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.002",
"Description": "攻击属性变化",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +29",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +30",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +31",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +32",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +34",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +36",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +38",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +40",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.003",
"Description": "体力 +45",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +29",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +30",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +31",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +32",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +34",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +36",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +38",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +40",
"Ratio": 100
},
{
"GroupLabel": "Bracelet.004",
"Description": "精神 +45",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "DP +30",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "DP +20",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "HP +20",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "力量 +2",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "灵巧 +2",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "体力 +2",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "精神 +2",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "智慧 +2",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "幸运 +2",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "DP +10",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "HP +10",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "HP +30",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "力量 +1",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "灵巧 +1",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "体力 +1",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "精神 +1",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "智慧 +1",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "幸运 +1",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "力量 +3",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "灵巧 +3",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "体力 +3",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "精神 +3",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "智慧 +3",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "幸运 +3",
"Ratio": 100
},
{
"GroupLabel": "LotteryTable.Common1",
"Description": "暴击率 +0.2%",
"Ratio": 100
}
]
}