本篇教程由作者设定使用 CC BY-NC 协议。

机械动力:永无止境添加了一套名为Chaotic Alchemy的系统,可以用原理不明的配方转换各种物品,但需要一类被称作Catalyst的物品作为催化剂。

而合成每种Catalyst的配方都根据世界种子在1296个配方中随机挑选,且无法使用JEI查询,因此发现合成Catalyst的正确配方是Chaotic Alchemy的核心玩法。

当尝试合成Catalyst失败时,可以从失败品中得到一些提示,本教程主要讲解如何编写计算机程序以发现正确的配方并减少尝试次数。

准备工作

在正式开始前,你需要完成整合包任务中的The Catharsis章节并自动化生产Inductive Mechanism。

然后,做以下准备:

  • 合成一个任意颜色的笼灯反相笼灯,一个机械手,一个Invar Machine,并把它们组装成Alchemical Laser。(JEI搜索Alchemical Laser,有组装说明)

  • 合成漏斗矿车,把它放在任意铁轨上,使Alchemical Laser对准它。

  • 合成流体灌装机离心分离机

  • 准备一个熔融玻璃来源,并有把熔融玻璃导入流体灌装机的方法。

  • 根据任务界面和JEI,准备一些能在流体灌装机中合成同类Reagent的物品。

  • 准备一个记录信息的方法,最好在游戏外,以免记录的信息被游戏内的各种意外破坏。

  • 如果你希望使用Chaotic Alchemy生产资源,你还需要准备动力搅拌器工作盆烈焰人燃烧室感应炉,Reagent Extractor。

  • (可选)在周围插几支巨型火把,以防敌对生物干扰你的研究。没见过苦力怕的玩家还以为这玩法操作不当会引发爆炸呢

注意事项

  • Alchemical Laser会伤害实体,所以不要把重要的实体放在它的路径上。

  • 不要让重要的实体被组成Alchemical Laser的机械手打到。

  • 不要使用频率过高的Alchemical Laser,否则可能会把漏斗矿车打坏。

  • 不要把实验失败获得的Mundane Alchemic Blend作为方块放置,否则它会丢失所有数据。

合成Catalyst的具体规则

要合成或尝试合成Catalyst,将四个Reagent放在漏斗矿车的前四格,然后启动Alchemical Laser,使其射向漏斗矿车。注意Reagent可以重复,且正确的配方可能包含重复的Reagent。

这将消耗所有放入的Reagent,若合成成功,合成所用材料对应的Catalyst将出现在漏斗矿车中,否则,出现的将会是记录了本次合成所用材料及其顺序的Mundane Alchemic Blend。

合成时可以选择在漏斗矿车的第五格放入Glowstone Accelerator或Redstone Accelerator,这对合成成功与否没有影响,但如果合成失败,这可能带来额外的信息。

提示信息规则

将合成失败所得的Mundane Alchemic Blend放入离心分离机,将根据以下规则获得荧石粉红石粉灰烬

  1. 对于每一个位置,如果尝试的配方与正确配方在该位置上有相同的物品,累加1份荧石粉,并将这两个物品标记为已配对。如果使用了Glowstone Accelerator,每次尝试会有一个触发了此判定的Reagent不被消耗。

  2. 对于尝试配方(正确配方)中的每一个没有被标记为已配对的物品,如果正确配方(尝试配方)中有相同且没有被标记为已配对的物品,累加1份红石粉,并将这两个物品标记为已配对。如果使用了Redstone Accelerator,每次尝试会有一个触发了此判定的Reagent不被消耗。

  3. 获得灰烬数量=4-(获得荧石粉数量+获得红石粉数量)。

如果不考虑使用Accelerator,这套规则与一个名为1A2B的猜数字游戏的规则完全相同,只是用物品,荧石粉和红石粉替换了数字,A和B。灰烬就是来凑数的

中文维基百科:1A2B

因此可以用1~6的数字来替换物品,xAyB来替换获得荧石粉和红石粉的数量,尝试4号,5号,3号,6号物品的配方相当于猜测数字4536,获得1份红石粉且没有获得荧石粉相当于获得提示0A1B。

问题转化为有6个可选数字,实际选4个且允许重复的1A2B游戏。

编写计算机程序以帮助解决问题

由于人很难最大限度利用获得的提示信息,编写一个计算机程序来解决问题是个不错的选择。

一个用于解决此问题的计算机程序应该能:

  • 根据用户输入的猜测和提示计算出可能的正确数字列表。(下文称为正确列表)

  • 根据现有的信息给出猜测建议,以降低平均猜测次数。

此外用户可能还希望它能给出多个猜测建议,以供用户选择其中一个,节省特定的Reagent。

根据用户输入的猜测和提示计算正确列表

计算用户的猜测对于输入前的正确列表中的每个数字应该获得的提示,如果此提示与用户输入的不同,将数字从正确列表中删除,否则保留。

如果删减后的正确列表没有任何数字,则说明发生错误,重置正确列表并提示用户重新输入。

根据现有的信息给出猜测建议

对于所有可能的猜测,计算正确列表中的每个数字对它给出的提示,并统计除4A0B外所有提示的数量。

很费计算资源?一台能运行Minecraft的设备不可能没有做这些计算需要的资源XD

用每个猜测统计出的提示数量的最大和最小值作差,并将猜测按照作出的差从小到大排列,差相同时包含在正确列表中的猜测优先。


方法的原理是,如果一个猜测被正确列表给出的其中一种提示较多,则获得此提示后正确列表也较(获得其他提示)长,而真正的正确数字是随机选取的,所以实际获得此提示的概率较大。

这导致猜测此数字后平均正确列表长度比猜测提示数量分布更均匀的数字长,可能需要更多次猜测来确定正确数字,增加了平均猜测次数。

而猜测任何数字在同一正确列表获得的提示总量不变,所以一个数字在正确列表获得的各提示数量的最大与最小值的差越小,猜测此数字获得的提示分布越均匀。

此外,猜测在正确列表中的数字有可能直接猜中真正的正确数字,因此我们希望作出的差相同时包含在正确列表中的猜测优先。


有明确的排序规则后,给出多个猜测建议只需在排序好的猜测中取多个猜测作为建议即可。

程序设计

  1. 定义一个函数用于对给定的猜测和正确数字计算提示。

  2. 初始化正确列表,使其包含每个可能的数字。(或在将要根据用户的第一个输入进行删减时将初始化与删减合并)

  3. 要求用户输入猜测的数字和获得的提示。

  4. 对于每个正确列表中的数字,如果其对用户输入的猜测给出的提示与用户输入的提示不同,移除它,否则保留。

  5. 如果正确列表中仅剩一个数字,输出它并结束程序,或回到第2步,重新初始化正确列表以使用户无需多次启动程序即可多次使用,否则继续执行程序。

  6. 对于所有可能的猜测,计算其在正确列表中获得的提示并统计每种提示的数量,存放于猜测列表中。

  7. 对于猜测列表中的每个猜测,作其获得的每种提示的数量中最大和最小值的差,并记录。

  8. 程序生成可能的猜测时通常有某种顺序,导致给出多个猜测建议不一定能帮助用户节省特定Reagent,因此在排序前需要随机打乱猜测列表,注意保持猜测与差的对应关系。

  9. 对猜测列表进行排序,注意仅以所作的差和是否在正确列表中为排序依据。

  10. 输出猜测列表中的前多个猜测,作为对下一次猜测的建议。

  11. 回到第3步,再次要求用户输入猜测的数字和获得的提示。

Python实现

预览正文时发现百科没法显示太长的单行代码,所以87行最后漏了个]没显示,但能复制出来。

另外笔者的水平并不好,这段代码只是能用的程度

from collections import Counter
from random import shuffle


def 计算提示(猜测配方, 正确配方):
    '''对于给定的猜测和正确配方,计算出应该获得的提示'''
    猜测_内部 = 猜测配方[:]#别问为啥有这两行
    正确_内部 = 正确配方[:]
    返回值 = [0, 0]#[荧石粉数量, 红石粉数量]
    for i in range(4):#计算荧石粉的数量
        if 猜测_内部[i] == 正确_内部[i]:
            返回值[0] += 1
            猜测_内部[i] = ''#移除配对成功的值,但不影响索引
            正确_内部[i] = 0#移除值时,将两个列表中的值修改为不同的值,以防移除的值被下一步配对
    for i in 正确_内部:#计算红石粉的数量
        if i in 猜测_内部:
            返回值[1] += 1
            猜测_内部.remove(i)#此处仅移除其中一个列表的值,以免影响循环
    return 返回值

#别问为啥有下面这三个函数
def updated(被更新字典, 更新源字典):
    '''更新字典且返回更新后的字典'''
    字典_内部 = 被更新字典
    字典_内部.update(更新源字典)
    return 字典_内部

def shuffled(目标列表):
    '''随机排列列表中的元素且返回随机排列后的列表'''
    列表_内部 = 目标列表
    shuffle(列表_内部)
    return 列表_内部

def 第一个元素(序列): return 序列[0]


剩余配方 = []
输入的猜测 = []
输入的提示 = []
可行的猜测 = []
在剩余配方中的可行的猜测 = []
不在剩余配方中的可行的猜测 = []

print('''此程序在Python3.7.7中编写,用于在CAB整合包的Chaotic Alchemy玩法中寻找Catalyst的合成配方
请输入6位数字,其中前4位是你尝试的配方,后两位是你获得的提示(荧石和红石的数量)
输入quit退出程序,输入reset重置程序''')


while True:
    用户输入 = input('>')#输入一个六位数字,前四位是尝试的配方,后两位是获得的提示
    
    if 用户输入 == 'quit':
        break
    elif 用户输入 == 'reset':
        剩余配方.clear()
        print('已重置')
        
    else:
        输入的猜测[:] = [int(i) for i in 用户输入[:4]]#拆分输入的配方和提示,并将其转换为列表
        输入的提示[:] = [int(i) for i in 用户输入[4:]]
        
        if len(剩余配方):#判断当前循环是否不是第一次猜测配方
            #如果不是第一次,对于当前剩余配方列表的每个配方,如果其对输入的猜测配方应有的提示和实际输入的提示不同,移除该配方,否则保留它
            剩余配方[:] = [i for i in 剩余配方 if 计算提示(输入的猜测, i) == 输入的提示]
        else:#如果是第一次,嵌套四个循环来生成所有可能的配方,并移除其中计算出的提示和输入的提示不同的配方
            剩余配方[:] = [[i1 + 1, i2 + 1, i3 + 1, i4 + 1] for i1 in range(6) for i2 in range(6) for i3 in range(6) for i4 in range(6) if 计算提示(输入的猜测, [i1 + 1, i2 + 1, i3 + 1, i4 + 1]) == 输入的提示]
            
        if len(剩余配方):#判断是否有剩余配方,用于在发生错误时发出提示
            if len(剩余配方) - 1:#判断剩余配方是否多于一个
                print('剩余{}个可能的配方'.format(len(剩余配方)))#如果是,输出剩余配方数量
                if len(剩余配方) < 10:#如果剩余配方少于十个,输出它们
                    print('它们分别是:', ' '.join([''.join([str(i1) for i1 in i2]) for i2 in 剩余配方]))
            else:#如果不是,配方已确定,输出该配方并自动重置程序
                print('配方已确定为{}'.format(''.join([str(i) for i in 剩余配方[0]])))
                剩余配方.clear()
                print('已自动重置')
                continue
        else:
            print('没有剩余的配方,你可能输入了错误的数据,或此程序存在漏洞')
            剩余配方.clear()
            print('已自动重置')
            continue

        #对于每个可行的猜测,统计剩余配方列表中的配方给出的除[4, 0]外每个可能的提示的数量
        在剩余配方中的可行的猜测[:] = [list(updated({(i3, i4): 0 for i3 in range(5) for i4 in range(5) if i3 + i4 < 5 and (i3, i4) not in [(4, 0), (3, 1)]}, Counter([tuple(计算提示(i1, i2)) for i2 in 剩余配方 if 计算提示(i1, i2) != [4, 0]])).values()) + [i1] for i1 in 剩余配方]
        #我们希望其他条件相同时在剩余配方列表中的猜测更优先,但又想让多个所有条件都相同的猜测随机排列,因此分别生成在和不在剩余配方列表中的猜测列表,分别随机排列并排序后归并,归并时在剩余配方列表中的猜测优先
        不在剩余配方中的可行的猜测[:] = [list(updated({(i6, i7): 0 for i6 in range(5) for i7 in range(5) if i6 + i7 < 5 and (i6, i7) not in [(4, 0), (3, 1)]}, Counter([tuple(计算提示([i1 + 1, i2 + 1, i3 + 1, i4 + 1], i5)) for i5 in 剩余配方 if 计算提示([i1 + 1, i2 + 1, i3 + 1, i4 + 1], i5) != [4, 0]])).values()) + [[i1 + 1, i2 + 1, i3 + 1, i4 + 1]] for i1 in range(6) for i2 in range(6) for i3 in range(6) for i4 in range(6) if [i1 + 1, i2 + 1, i3 + 1, i4 + 1] not in 剩余配方]
        #对于每个可行的猜测,用此猜测获得的最多的提示与其获得的最少的提示作差,并按所得的差从小到大对猜测排序,别问为啥往排序里加个key,问就是这排序函数发现第一个依据相同时默认会找第二个依据来排序,但这不是我们想要的
        在剩余配方中的可行的猜测[:] = sorted(shuffled([[max(i[:-1]) - min(i[:-1]), i[-1]] for i in 在剩余配方中的可行的猜测]), key = 第一个元素)
        不在剩余配方中的可行的猜测[:] = sorted(shuffled([[max(i[:-1]) - min(i[:-1]), i[-1]] for i in 不在剩余配方中的可行的猜测]), key = 第一个元素)

        可行的猜测.clear()
        while len(在剩余配方中的可行的猜测) and len(不在剩余配方中的可行的猜测):
            if 在剩余配方中的可行的猜测[0][0] > 不在剩余配方中的可行的猜测[0][0]:#相等的情况包含在else中,因此在剩余配方列表中的猜测会更优先
                可行的猜测.append(不在剩余配方中的可行的猜测.pop(0))
            else:
                可行的猜测.append(在剩余配方中的可行的猜测.pop(0))
        可行的猜测.extend(在剩余配方中的可行的猜测)
        可行的猜测.extend(不在剩余配方中的可行的猜测)
        
        print('建议猜测:', ' '.join([''.join([str(i1) for i1 in i2[1]]) for i2 in 可行的猜测[:10]]))#输出最靠前的多个猜测作为建议