mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
201 lines
7.5 KiB
Lua
201 lines
7.5 KiB
Lua
#!/usr/bin/env luajit
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local H = require("tests.link3_harness")
|
|
local Cache = require("tests.game3_cache")
|
|
|
|
local failed = 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
local function eq(a, b, msg)
|
|
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
|
end
|
|
|
|
if not Cache.root("meta.json") then
|
|
print("[skip] Gen 3 link lockstep runs the real battle engine on ROM data: " .. tostring(Cache.reason))
|
|
os.exit(0)
|
|
end
|
|
H.mountCache()
|
|
|
|
local function mon(species, level, extra)
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local moves = Pokemon.movesAtLevel(species, level)
|
|
local m = {
|
|
species = species, level = level, moves = moves, personality = extra and extra.personality or 0,
|
|
ivs = { hp = 20, atk = 20, def = 20, spe = 20, spa = 20, spd = 20 },
|
|
evs = { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 },
|
|
item = extra and extra.item or 0,
|
|
}
|
|
return H.legal(m)
|
|
end
|
|
|
|
local function session(name, trainerId, gender)
|
|
return { name = name, trainerId = trainerId, gender = gender, party = {}, bag = {} }
|
|
end
|
|
|
|
local function singlesPolicy(w, st)
|
|
local b = st.player
|
|
local mon0 = b and b.mon or {}
|
|
local usable = {}
|
|
for i = 1, 4 do
|
|
if mon0.moves and mon0.moves[i] and (tonumber(mon0.pp and mon0.pp[i]) or 0) > 0 then usable[#usable + 1] = i end
|
|
end
|
|
local slot = usable[((st.turn or 0) % math.max(1, #usable)) + 1] or 1
|
|
w.Ui._pendingCommand = w.Commands.playerAction(st, 1, slot)
|
|
end
|
|
|
|
local function doublesPolicy(w, st)
|
|
local sel = w.Battle._dblSel
|
|
if not (sel and sel.active) then return end
|
|
local id = sel.active
|
|
local b = w.State.battler(st, id)
|
|
local mon0 = b and b.mon or {}
|
|
local slot = 1
|
|
for i = 1, 4 do
|
|
if mon0.moves and mon0.moves[i] and (tonumber(mon0.pp and mon0.pp[i]) or 0) > 0 then
|
|
slot = i
|
|
if (st.turn + id) % 2 == 0 then break end
|
|
end
|
|
end
|
|
local target = w.State.isPresent(st, 1) and 1 or 3
|
|
w.Ui._pendingCommand = w.Commands.playerAction(st, 1, slot, id, target)
|
|
end
|
|
|
|
local function runMatch(opts)
|
|
local relay = H.relay({ seed = opts.seed, seats = 2, lag = opts.lag or {}, roomSeed = opts.seed })
|
|
local partyA = opts.partyA
|
|
local partyB = opts.partyB
|
|
local w0 = H.newWorld("seat0", session("RED", 0x1234, 0))
|
|
local w1 = H.newWorld("seat1", session("LEAF", 0x5678, 1))
|
|
local ws = H.newWorld("spectator", session("WATCHER", 0x9999, 0))
|
|
H.attachSeat(w0, relay, 0, { mode = opts.mode, myParty = H.pack(partyA),
|
|
profile = { rule = {} } })
|
|
H.attachSeat(w1, relay, 1, { mode = opts.mode, myParty = H.pack(partyB),
|
|
profile = { rule = {} } })
|
|
if not opts.lateSpectator then H.attachSpectator(ws, relay, { mode = opts.mode, profile = { rule = {} } }) end
|
|
local policy = opts.mode == "double" and doublesPolicy or singlesPolicy
|
|
local frames, spectating = 0, not opts.lateSpectator
|
|
while frames < 20000 do
|
|
frames = frames + 1
|
|
for _ = 1, opts.stepsA or 1 do H.step(w0, policy) end
|
|
for _ = 1, opts.stepsB or 1 do H.step(w1, policy) end
|
|
if not spectating then
|
|
local st = H.run(w0, function() return w0.Battle.getState() end)
|
|
if st and (st.turn or 0) >= 3 then
|
|
H.attachSpectator(ws, relay, { mode = opts.mode, profile = { rule = {} } })
|
|
spectating = true
|
|
end
|
|
end
|
|
if spectating then H.step(ws, nil) end
|
|
relay:tick()
|
|
if w0.result and w1.result and (ws.result or not spectating) then break end
|
|
end
|
|
return w0, w1, ws, relay, frames
|
|
end
|
|
|
|
local function hashesOf(w)
|
|
return H.run(w, function()
|
|
local out = {}
|
|
for turn, value in pairs(w.LB._myHashes or {}) do out[turn] = value end
|
|
return out
|
|
end)
|
|
end
|
|
|
|
local function sameHashes(a, b)
|
|
local n = 0
|
|
for turn, value in pairs(a) do
|
|
if b[turn] ~= nil then
|
|
n = n + 1
|
|
if b[turn] ~= value then return false, n, turn end
|
|
end
|
|
end
|
|
return true, n
|
|
end
|
|
|
|
local MIRROR = { win = "lose", lose = "win", draw = "draw" }
|
|
|
|
local function verify(label, w0, w1, ws, relay)
|
|
eq(w0.result ~= nil and w1.result ~= nil, true, label .. ": both seats finished")
|
|
eq(MIRROR[w0.result], w1.result, label .. ": the results mirror (" .. tostring(w0.result) .. "/" .. tostring(w1.result) .. ")")
|
|
local r0 = H.run(w0, function() return w0.LB.endReason end)
|
|
local r1 = H.run(w1, function() return w1.LB.endReason end)
|
|
eq(r0, nil, label .. ": seat 0 saw no desync")
|
|
eq(r1, nil, label .. ": seat 1 saw no desync")
|
|
local h0, h1, hs = hashesOf(w0), hashesOf(w1), hashesOf(ws)
|
|
local ok, n, turn = sameHashes(h0, h1)
|
|
check(ok and n >= 2, label .. ": every turn's digest matched across seats (" .. tostring(n) .. " turns" .. (turn and (", split at " .. turn) or "") .. ")")
|
|
local okS, nS = sameHashes(h0, hs)
|
|
check(okS and nS >= 1, label .. ": the spectator's own simulation digested the same (" .. tostring(nS) .. " turns)")
|
|
eq(ws.result, "ended", label .. ": the spectator watched to the end")
|
|
eq(#w0.reports, 1, label .. ": seat 0 reported once")
|
|
eq(#w1.reports, 1, label .. ": seat 1 reported once")
|
|
eq(MIRROR[w0.reports[1]], w1.reports[1], label .. ": and the reports mirror")
|
|
eq(#ws.reports, 0, label .. ": the spectator never reports")
|
|
local spectatorSent = false
|
|
for _, row in ipairs(relay.log) do if row.seat == nil then spectatorSent = true end end
|
|
check(not spectatorSent, label .. ": the spectator never sent a message")
|
|
local kinds = {}
|
|
for _, row in ipairs(relay.log) do kinds[row.msg.type] = (kinds[row.msg.type] or 0) + 1 end
|
|
check((kinds.game3_battle_hash or 0) >= 4, label .. ": hashes crossed the relay")
|
|
check((kinds.game3_battle_outcome or 0) == 2, label .. ": each seat sent one game3_battle_outcome")
|
|
local st0 = H.run(w0, function() return w0.Battle.getState() end)
|
|
local stS = H.run(ws, function() return ws.Battle.getState() end)
|
|
local hp0, hpS = {}, {}
|
|
for i, m in ipairs(st0.playerParty) do hp0[i] = tonumber(m.hp) end
|
|
for i, m in ipairs(stS.playerParty) do hpS[i] = tonumber(m.hp) end
|
|
eq(table.concat(hp0, ","), table.concat(hpS, ","), label .. ": the spectator's view of seat 0's party ends the same")
|
|
end
|
|
|
|
print("[test] 1. a singles arena match over the relay, both seats and a spectator")
|
|
do
|
|
local w0, w1, ws, relay = runMatch({
|
|
seed = 0x13579, mode = "single",
|
|
partyA = { mon(6, 50), mon(25, 50), mon(131, 50) },
|
|
partyB = { mon(9, 50), mon(3, 50), mon(143, 50) },
|
|
})
|
|
verify("singles", w0, w1, ws, relay)
|
|
end
|
|
|
|
print("[test] 2. the same with lag and uneven frame rates on each side")
|
|
do
|
|
local w0, w1, ws, relay = runMatch({
|
|
seed = 0x2468A, mode = "single", lag = { [0] = 2, [1] = 0, spectator = 3 }, stepsA = 3, stepsB = 1,
|
|
partyA = { mon(94, 50), mon(65, 50) },
|
|
partyB = { mon(68, 50), mon(59, 50) },
|
|
})
|
|
verify("singles lag", w0, w1, ws, relay)
|
|
end
|
|
|
|
print("[test] 3. a doubles arena match with a spectator")
|
|
do
|
|
local w0, w1, ws, relay = runMatch({
|
|
seed = 0x5A5A5, mode = "double",
|
|
partyA = { mon(6, 50), mon(9, 50), mon(3, 50) },
|
|
partyB = { mon(130, 50), mon(143, 50), mon(65, 50) },
|
|
})
|
|
verify("doubles", w0, w1, ws, relay)
|
|
end
|
|
|
|
print("[test] 4. a spectator who joins at turn 3 replays the log and catches up")
|
|
do
|
|
local w0, w1, ws, relay = runMatch({
|
|
seed = 0x777, mode = "single", lateSpectator = true,
|
|
partyA = { mon(149, 50), mon(26, 50) },
|
|
partyB = { mon(248, 50), mon(59, 50) },
|
|
})
|
|
verify("late spectator", w0, w1, ws, relay)
|
|
end
|
|
|
|
if failed == 0 then
|
|
print("[pass] link3 lockstep")
|
|
os.exit(0)
|
|
end
|
|
print("[fail] link3 lockstep: " .. failed)
|
|
os.exit(1)
|