Skip to content

Instantly share code, notes, and snippets.

@taotao54321
Created November 22, 2023 19:21
Show Gist options
  • Save taotao54321/146d584ce44f4a2e73e50ac7ca4a0717 to your computer and use it in GitHub Desktop.
Save taotao54321/146d584ce44f4a2e73e50ac7ca4a0717 to your computer and use it in GitHub Desktop.
NES Quarth (J): block chunk manipulation script for SubNesHawk
--[[
クォース (FC) ツモ調整 for SubNesHawk
入力を微調整し、望みのツモ ($98) が出るパターンを探索する。
使い方:
* 調整開始フレーム以降の入力を本スクリプトと同じディレクトリに manip.in として置く。
* 調整開始フレームで本スクリプトを起動する。解は manip.out に出力される。
--]]
local movie_ext = require("movie_ext")
local util = require("util")
-- プレイヤーの挙動に影響しない範囲で入力を変更できるかどうかを返す。
local function can_mutate(input)
-- 1P/2Pの U 以外のキーが押されていれば変更不能とみなす。
for i = 1, 2 do
for button, _v in pairs(input[i]) do
if button ~= "Up" then
return false
end
end
end
return true
end
-- プレイヤーの挙動に影響しない範囲で入力を確率的に変異させる。
local function mutate_input(input)
-- 変更不能な入力はそのままにする。
if not can_mutate(input) then
return input
end
local buttons1 = util.table_clone(input[1])
local buttons2 = util.table_clone(input[2])
-- 以下のいずれかの変更を確率的に施す:
--
-- * 1Pの U を2Pの U に置き換える (元入力は1P側で U を押していると仮定)
-- * 1Pの S を押す
-- * 2Pの S を押す
local r = math.random()
if r < 0.1 then
if buttons1["Up"] then
buttons1["Up"] = false
buttons2["Up"] = true
end
elseif r < 0.2 then
buttons1["Select"] = true
elseif r < 0.3 then
buttons2["Select"] = true
end
return { buttons1, buttons2 }
end
-- inputs を微調整し、ツモ調整を試みる。
-- 望みのツモ tumo_want が得られたら調整後の入力列を、さもなくば nil を返す。
local function try_manipulate(inputs, tumo_want)
local inputs_manip = {}
local tumo = nil
local handle = event.on_bus_exec(function ()
tumo = emu.getregister("A")
end, 0xAC7B)
for _i, input in ipairs(inputs) do
local input_manip = mutate_input(input)
table.insert(inputs_manip, input_manip)
util.run_frame(input_manip)
end
event.unregisterbyid(handle)
assert(tumo ~= nil)
if tumo == tumo_want then
return inputs_manip
else
return nil
end
end
local function main()
local PATH_IN = "manip.in"
local PATH_OUT = "manip.out"
-- 望みのツモ ($98)。
--local TUMO_WANT = 0
local TUMO_WANT = 0x09
--local TUMO_WANT = 0x0D
-- 開始フレーム以降の入力列を読み込む。
local inputs = movie_ext.parse_inputs_from_file(PATH_IN)
-- 開始時のステートをセーブ。
local state = memorysavestate.savecorestate()
-- 成功するまで調整を繰り返す。
for i = 1, 10000000 do
if i % 100 == 0 then
print(string.format("iteration %d", i))
end
local inputs_manip = try_manipulate(inputs, TUMO_WANT)
if inputs_manip ~= nil then
-- 成功したら調整後の入力列を保存してループ終了。
movie_ext.format_inputs_to_file(PATH_OUT, inputs_manip)
break
end
memorysavestate.loadcorestate(state)
end
client.pause()
end
main()
--[[
ムービーファイル中の入力列を自前で読み書きする。
--]]
local movie_ext = {}
local BUTTON_CHARS = { "U", "D", "L", "R", "S", "s", "B", "A" }
local BUTTON_NAMES = { "Up", "Down", "Left", "Right", "Start", "Select", "B", "A" }
-- 文字列 s 内のバイトインデックス i (1-based) の文字を返す。ASCII 文字列を仮定している。
local function str_at(s, i)
return string.sub(s, i, i)
end
local function panic(msg)
error(msg)
end
-- 1 人のプレイヤーの入力キー文字列 ("UDLRSsBA" 形式) をパースし、その入力を返す。
local function parse_buttons(s)
local buttons = {}
for i = 1, 8 do
local c = str_at(s, i)
if c ~= "." then
buttons[BUTTON_NAMES[i]] = true
end
end
return buttons
end
-- 1 フレームの入力をパースし、1P/2P入力を返す。
local function parse_input(line)
local PATTERN_BUTTONS = "[U.][D.][L.][R.][S.][s.][B.][A.]"
local PATTERN = string.format("^|[^|]*|(%s)|(%s)|$", PATTERN_BUTTONS, PATTERN_BUTTONS)
local input = {}
local s1, s2 = string.match(line, PATTERN)
local buttons1 = parse_buttons(s1)
local buttons2 = parse_buttons(s2)
table.insert(input, buttons1)
table.insert(input, buttons2)
return input
end
-- ファイル全体を入力列としてパースする。
movie_ext.parse_inputs_from_file = function (path)
local rdr = io.open(path, "r")
if rdr == nil then
panic(string.format("can't open file '%s' to read", path))
end
local inputs = {}
for line in rdr:lines() do
local input = parse_input(line)
table.insert(inputs, input)
end
rdr:close()
return inputs
end
-- 1 人のプレイヤーの入力を "UDLRSsBA" 形式にフォーマットする。
local function format_buttons(buttons)
local s = ""
for i = 1, 8 do
if buttons[BUTTON_NAMES[i]] then
s = s .. BUTTON_CHARS[i]
else
s = s .. "."
end
end
return s
end
-- 1P/2P入力を 1 フレームの入力文字列としてフォーマットする。
local function format_input(input)
local s1 = format_buttons(input[1])
local s2 = format_buttons(input[2])
return string.format("| 0,..|%s|%s|", s1, s2)
end
-- 入力列をムービーファイル用の形式にフォーマットし、ファイルに保存する。
movie_ext.format_inputs_to_file = function (path, inputs)
local wtr = io.open(path, "w")
if wtr == nil then
panic(string.format("can't open file '%s' to write", path))
end
for _i, input in ipairs(inputs) do
local line = format_input(input)
wtr:write(line)
wtr:write("\n")
end
wtr:close()
end
return movie_ext
local util = {}
util.panic = function (msg)
error(msg)
end
-- テーブルをシャローコピーする。
util.table_clone = function (tbl)
local res = {}
for k, v in pairs(tbl) do
res[k] = v
end
return res
end
-- 2 つの配列が等しいかどうかを返す。
util.array_eq = function (lhs, rhs)
if #lhs ~= #rhs then
return false
end
for i = 1, #lhs do
if lhs[i] ~= rhs[i] then
return false
end
end
return true
end
-- 配列 ary の指定範囲のスライスを返す。idx は 1-based。
util.array_slice = function (ary, idx, len)
local slice = {}
for i = idx, idx + len - 1 do
table.insert(slice, ary[i])
end
return slice
end
-- 与えられた1P/2P入力で 1 フレーム進める。
util.run_frame = function (input)
for i = 1, 2 do
joypad.set(input[i], i)
end
emu.frameadvance()
end
return util
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment