火焰纹章6-8(GBA几作)有这么几个设定:人物死了就没法再用了;招募部分人物需要特定人物存活;战斗回避、暴击,升级属性成长都由Random Number Generator(RNG)决定。

RNG不是真随机,但是模拟器里光S&L不能改变结果,还要做消耗RNG的操作:一个简单消耗RNG数列的方式是让游戏计算移动轨迹,但是这需要玩家记住规律、记录High(>50)和Low(<50)、反复S&L,很费时。

玩着游戏做这些很影响体验,所以有了这篇——精准控制结果。

RNG原理比较简单,通过几个数字和特定算法生成数列,在战斗中消耗该数列计算结果:

  • 命中:取接下来2个数字的平均值,如果低于命中率代表命中
  • 暴击:取接下来的1个数字,如果低于暴击率就暴击,造成3倍伤害
  • 如果敌方没死就按照同样的流程计算反击的命中和暴击,根据速度判断追击
  • 结算后如果升级了,取接下来7个数字决定7个属性加不加

简单场景

最简单的场景:尾刀命中、暴击杀死,升级全属性增加。

我们需要接下来的:

命中 暴击 升级
LL L LLLLLLL

L是Low的缩写,代表小于判定值,即判定通过。

用Python简单暴力跑一个:

import math

acc = 75 # accuracy - dodge
rr = 20 # attr raise rate
crit = 5

def nextrng(r1, r2, r3):
    i = (r3 >> 5) ^ (r2 << 11) ^ (r1 << 1) ^ (r2 >> 15)
    j = i & 0xffff
    return j

def rngsim(base):
    r1, r2, r3 = base
    result = [r3, r2, r1]

    for i in range(4, 20):
        n = nextrng(result[i-4], result[i-3], result[i-2])
        result.append(n)

    result = [math.floor(x/655.36) for x in result]
    return result[3:]

def rngok(result):
    hit = result[0] + result[1]
    if hit > 2 * acc:
        return False

    if result[2] > crit:
        return False

    # [3, 9]
    for i in range(3, 10):
        if result[i] > rr:
            return False
    return True

for i in range(1, 100):
    for j in range(1, 100):
        for k in range(1, 100):
            base = (i, j, k)
            result = rngsim(base)
            if rngok(result):
                print(result)
                print(base)
                break

经计算, 1 32 512作为seed符合要求,RNG table为[1, 3, 0, 3, 9, 6, 6, 0, 6, 0, 1, 37, 12, 77, 1, 0]

复杂场景

不小心走位失误,此时角色会被一刀砍死,读档又要重新玩很久,你不希望这种事情发生,此时拯救角色需要的RNG table为:

敌方命中 反击命中 暴击 升级
HH LL L LLLLLLL

修改rngok,依次判断result里的数值,再跑一遍:

def rngok(result):
    # enemy miss
    if (result[0]+result[1]) / 2 > miss:
        return False
    
    # 略
    return True

实际修改

了解原理后,还需要工具来修改内存:一个支持lua的gba模拟器。

我嫌现成脚本使用过于复杂,就改了改:去掉了输入模式(想改就改lua里memory.writeword后数字),按M直接改RNG table,按N产生随机数填充RNG table。

while true do
    filter = 1
    local nsim = 20
    rngs = rngsim(503)
    for i = 1, nsim do
        gui.text(228, 8*(i-1), string.format("%3d", rngs[i]/655.36))
    end
    --gui.text(0,0,"Filter Mode: ")

    c = input.get()
    -- M: rng for mine, N: aNother(enemy)
    -- write rng base to control
    local rngbase = 0x03000000
    if c.M then
        memory.writeword(rngbase, 1)
        memory.writeword(rngbase+2, 1)
        memory.writeword(rngbase+4, 32)
    elseif c.N then
        -- reset
        math.randomseed(os.clock() * 1000)
        memory.writeword(rngbase, math.random(1, 65535))
        memory.writeword(rngbase+2, math.random(1, 65535))
        memory.writeword(rngbase+4, math.random(1, 65535))
    end

    for i = 1, 10, 1 do
        --gui.text(0,8+(i*8),rn[i])
        --gui.text(20,8+(i*8),op[i])
        if hit[i] == true then
            --gui.text(40,8+(i*8),"<- Hit")
        end
    end
    if rn[1] ~= "" and inputs == 0 then
        dis = emptyarray(dis,3)
        compareRN()
    end
    n = input.get()

    -- just to show it's working
    if rn[1] ~= "" then
        if dis[1] == "" then
            gui.text(0,104,"Distance: ---")
        elseif dis[1] ~= "" and dis[2] == "" then
            gui.text(0,104,"Distance: " .. dis[1])
        elseif dis[1] ~= "" and dis[2] ~= "" and dis[3] == "" then
            gui.text(0,104,string.format("Distance: %d - %d",dis[1],dis[2]))
        else

            gui.text(0,104,string.format("Distance: %d - %d - %d",dis[1],dis[2],dis[3]))

        end
    end
    emu.frameadvance()
end