Files
gen1recomp/tests/drivers/g3battle_multi_arena.lua
2026-09-24 05:17:32 -04:00

361 lines
16 KiB
Lua

local U = require("tests.drivers.util")
local H = require("tests.link3_harness")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/g3battle_multi"
local failures = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
if failures == 0 then
print("PASS g3battle_multi_arena")
love.event.quit(0)
else
print("FAIL g3battle_multi_arena failures=" .. failures)
love.event.quit(1)
end
end
local function mon(species, level, moves)
return H.legal({
species = species, level = level, moves = moves, personality = 0x51,
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 = 0,
nickname = "", friendship = 70,
})
end
local NAMES = { [0] = "RED", [1] = "LEAF", [2] = "BLUE", [3] = "GREEN" }
local TRAINER_IDS = { [0] = 0x1234, [1] = 0x2468, [2] = 0x1357, [3] = 0x8642 }
local function profile()
return { engine = 3, version = "firered", kind = "vanilla", rulesetId = "g3_multi", rule = { partySize = 3 } }
end
local function players()
local out = {}
for seat = 0, 3 do out[#out + 1] = { id = string.format("%08x", seat + 1), name = NAMES[seat], seat = seat } end
return out
end
return function(game)
print("PASS driver_started")
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
local Battle = require("src.core.game3.battle")
local BattleUi = require("src.core.game3.battle.ui")
local State = require("src.core.game3.battle.state")
local Engine = require("src.core.game3.battle.engine")
local LB = require("src.core.game3.link.battle")
local parties = {
[0] = { mon(6, 50, { 53, 17 }), mon(25, 50, { 85, 98 }), mon(131, 50, { 57, 58 }) },
[1] = { mon(9, 50, { 57, 44 }), mon(3, 50, { 76, 22 }), mon(143, 50, { 34, 29 }) },
[2] = { mon(65, 50, { 94, 93 }), mon(68, 50, { 2, 67 }), mon(94, 50, { 85, 95 }) },
[3] = { mon(59, 50, { 53, 44 }), mon(130, 50, { 57, 44 }), mon(26, 50, { 85, 98 }) },
}
local packed = {}
for seat = 0, 3 do packed[seat] = H.pack(parties[seat]) end
local SEED = 0x3C3C5A5A
local returned
game.returnToLauncher = function(o) returned = o or {} end
local function canon(st, id)
return st.linkSeatOf[id]
end
local function scriptedPeers(relay, mySeat)
local peers, sent, switched, outcomes = {}, {}, {}, {}
for seat = 0, 3 do
if seat ~= mySeat then peers[seat] = relay:session(seat) end
end
local seatSent = {}
return function()
for seat, s in pairs(peers) do
for _, msg in ipairs(s:poll()) do
if msg.seat == mySeat then
if msg.type == "game3_hello" then
local hello = LB.copy(msg)
hello.seat = nil
hello.name = NAMES[seat]
hello.game3.trainerId, hello.game3.gender, hello.game3.seat = TRAINER_IDS[seat], seat % 2, seat
s:send(hello)
elseif msg.type == "game3_battle_linkup" then
s:send({ type = "game3_battle_linkup", linkType = msg.linkType, players = 4 })
elseif msg.type == "game3_battle_seat" and not seatSent[seat] then
seatSent[seat] = true
s:send({ type = "game3_battle_seat" })
elseif msg.type == "game3_battle_setup" then
s:send({ type = "game3_battle_setup", mode = "multi", unionRoom = false, name = NAMES[seat],
trainerId = TRAINER_IDS[seat], gender = seat % 2, party = packed[seat] })
elseif msg.type == "game3_battle_hash" then
s:send({ type = "game3_battle_hash", turn = msg.turn, value = msg.value, parts = msg.parts })
elseif msg.type == "game3_battle_outcome" and not outcomes[seat] then
outcomes[seat] = true
local out = msg.outcome
if seat % 2 ~= mySeat % 2 then out = ({ [1] = 2, [2] = 1, [3] = 3, [128] = 1 })[msg.outcome] or 3 end
s:send({ type = "game3_battle_outcome", outcome = out, turn = msg.turn })
end
end
end
end
local st = Battle.getState()
if Battle.isActive() and st and st.multi then
if Battle._phase == "linkwait" and Battle._linkMulti then
for id = 0, 3 do
local seat = st.linkSeatOf[id]
local key = tostring(seat) .. ":" .. tostring(st.turn)
if peers[seat] and not sent[key] and State.isPresent(st, id) then
sent[key] = true
local b = State.battler(st, id)
local slot = 1
for i = 1, 4 do
if b.mon.moves[i] and (tonumber(b.mon.pp and b.mon.pp[i]) or 0) > 0 then slot = i break end
end
local target = State.isPresent(st, State.OPPOSITE(id)) and State.OPPOSITE(id)
or State.PARTNER(State.OPPOSITE(id))
peers[seat]:send({ type = "game3_battle_action", turn = st.turn, kind = "move", slot = slot,
target = canon(st, target) })
end
end
end
local pending = Battle._linkSwitch
if Battle._phase == "linkswitch" and pending and pending.battler ~= nil then
local seat = st.linkSeatOf[pending.battler]
local key = tostring(seat) .. ":" .. tostring(st.turn) .. ":" .. tostring(pending.battler)
if peers[seat] and not switched[key] then
switched[key] = true
local cands = Engine.replacementCandidates(st, pending.battler)
if cands[1] then peers[seat]:send({ type = "game3_battle_switch", slot = cands[1] }) end
end
end
end
relay:tick()
end
end
local function runArena(mySeat, shots)
returned = nil
local relay = H.relay({ seed = SEED, seats = 4, roomSeed = 10 + mySeat })
local session = relay:session(mySeat)
local peerStep = scriptedPeers(relay, mySeat)
local outcome
local digests
game:enterArena({
profile = profile(), role = ({ [0] = "host", [1] = "guest", [2] = "seat2", [3] = "seat3" })[mySeat],
seat = mySeat, seats = 4, seed = SEED, match = "m" .. mySeat, room = relay.room, players = players(),
myParty = packed[mySeat], session = session, client = relay:client(), mode = "multi",
onDone = function(r)
outcome = r
digests = {}
for turn, value in pairs(LB._myHashes or {}) do digests[turn] = value end
end,
})
if game.session then
game.session.name, game.session.gender = NAMES[mySeat], mySeat % 2
end
local taken, info = {}, {}
local lastAction = -1
local partyOpened = false
for _ = 1, 40000 do
if returned then break end
peerStep()
local st = Battle.getState()
if Battle.isActive() and st and st.multi then
info.own = st.linkOwn
local PartyMenu = package.loaded["src.ui.game3.party_menu"]
local partyOpen = PartyMenu and PartyMenu.isOpen and PartyMenu.isOpen()
local menuUp = Battle._phase == "command" and BattleUi._mode == "menu" and not partyOpen
local Message = package.loaded["src.ui.game3.message"]
local page = (Message and Message.isOpen and Message.isOpen() and Message.isWaiting and Message.isWaiting()
and tostring(Message.currentPage() or "")) or ""
page = page:gsub("\n", " ")
if shots.result and not taken.result and (page:find("defeated", 1, true) or page:find("lost to", 1, true)
or page:find("lost against", 1, true) or page:find("draw", 1, true)) then
taken.result = true
info.resultText = page
U.still(game, DIR .. "/" .. shots.result)
elseif shots.intro and not taken.intro and page:find("want to battle", 1, true) then
taken.intro = true
info.introText = page
local tp = require("src.core.game3.battle.anim").stage().trainer.player
info.backs = { own = tp.gender, partner = tp.gender2 }
U.wait(20)
U.still(game, DIR .. "/" .. shots.intro)
elseif shots.sendout and not taken.sendout and page:find("Go! ", 1, true) then
taken.sendout = true
info.sendoutText = page
U.wait(10)
U.still(game, DIR .. "/" .. shots.sendout)
elseif shots.start and not taken.start and menuUp then
taken.start = true
U.wait(8)
U.still(game, DIR .. "/" .. shots.start)
elseif shots.party and not taken.party and menuUp and not partyOpened then
partyOpened = true
U.tap(game, "down")
U.wait(4)
U.tap(game, "a")
U.wait(4)
elseif shots.party and not taken.party and partyOpen then
U.wait(40)
info.order = BattleUi.battlePartyOrder(st)
info.allies = BattleUi.allySlots(st, info.order)
U.still(game, DIR .. "/" .. shots.party)
taken.party = true
U.tap(game, "b")
U.wait(20)
if PartyMenu.isOpen() then U.tap(game, "b") U.wait(10) end
elseif not partyOpen and Battle._phase == "command"
and (BattleUi._mode == "menu" or BattleUi._mode == "moves" or BattleUi._mode == "target")
and U.frame() - lastAction > 6 and (taken.party or not shots.party) then
lastAction = U.frame()
if BattleUi._mode == "menu" and (tonumber(BattleUi._menuIndex) or 1) ~= 1 then
U.tap(game, "up")
U.wait(2)
U.tap(game, "left")
else
U.tap(game, "a")
end
elseif partyOpen and taken.party and PartyMenu.mode == "battle_faint" then
local order = BattleUi.battlePartyOrder(st)
local pick
for view, pi in ipairs(order) do
local m = st.playerParty[pi]
if not pick and st.partyOwner.player[pi] == st.linkOwn and m and (tonumber(m.hp) or 0) > 0 then pick = view end
end
if pick then PartyMenu.cursor = pick end
U.tap(game, "a")
U.wait(8)
U.tap(game, "a")
U.wait(8)
elseif partyOpen and taken.party then
U.tap(game, "b")
U.wait(4)
else
U.wait(1)
end
else
U.wait(1)
end
end
return outcome, digests, taken, info, relay
end
local r0, d0, shots0, info0, relay0 = runArena(0, {
intro = "g3battle_multi_seat0_intro_two_trainers.png",
sendout = "g3battle_multi_seat0_partner_sendout.png",
start = "g3battle_multi_seat0_battle_start.png",
party = "g3battle_multi_seat0_party_menu.png",
result = "g3battle_multi_seat0_result.png",
})
result(shots0.result, "seat 0 multi result was captured (" .. tostring(info0.resultText) .. ")")
result(shots0.intro and (info0.introText or ""):find("LEAF and GREEN", 1, true) ~= nil,
"seat 0 intro names both link opponents (" .. tostring(info0.introText) .. ")")
result(shots0.sendout and (info0.sendoutText or ""):find("BLUE sent out", 1, true) ~= nil,
"seat 0 intro has the partner send out first (" .. tostring(info0.sendoutText) .. ")")
result(r0 == "win" or r0 == "lose" or r0 == "draw", "seat 0 multi battle finished (" .. tostring(r0) .. ")")
result(returned and returned.tab == "online", "seat 0 returned to the launcher's online tab")
result(shots0.start and shots0.party, "seat 0 multi battle start and party menu were captured")
result(info0.own == 0, "seat 0 controls battler 0")
local own0 = {}
for view, allied in pairs(info0.allies or {}) do if allied then own0[#own0 + 1] = view end end
table.sort(own0)
result(table.concat(own0, ",") == "2,5,6", "seat 0 party menu tints the ally's boxes 2, 5 and 6 (" .. table.concat(own0, ",") .. ")")
local n0 = 0
for _ in pairs(d0 or {}) do n0 = n0 + 1 end
result(n0 >= 1, "seat 0 digested " .. n0 .. " turns")
local actions = {}
for _, row in ipairs(relay0.log) do
if row.msg.type == "game3_battle_action" and row.seat == 0 then
actions[#actions + 1] = row.msg
end
end
local single = #actions > 0
for _, a in ipairs(actions) do if a.actions ~= nil or a.kind == "list" then single = false end end
result(single, "seat 0 sent one action per turn for its own battler (" .. #actions .. ")")
local r2, d2, shots2, info2 = runArena(2, {
intro = "g3battle_multi_seat2_intro_two_trainers.png",
start = "g3battle_multi_seat2_battle_start.png",
party = "g3battle_multi_seat2_party_menu.png",
})
result(r2 == "win" or r2 == "lose" or r2 == "draw", "seat 2 multi battle finished (" .. tostring(r2) .. ")")
result(shots2.start and shots2.party, "seat 2 multi battle start and party menu were captured")
result(info2.own == 2, "seat 2 controls battler 2")
local r1, _, shots1, info1 = runArena(1, {
intro = "g3battle_multi_seat1_intro_two_trainers.png",
sendout = "g3battle_multi_seat1_partner_sendout.png",
start = "g3battle_multi_seat1_battle_start.png",
party = "g3battle_multi_seat1_party_menu.png",
})
result(shots1.intro and (info1.introText or ""):find("RED and BLUE", 1, true) ~= nil,
"seat 1 intro names seat 0 then seat 2 (" .. tostring(info1.introText) .. ")")
result(shots1.sendout and (info1.sendoutText or ""):find("GREEN sent out", 1, true) ~= nil,
"seat 1 intro has its partner seat 3 send out first (" .. tostring(info1.sendoutText) .. ")")
result(r1 == "win" or r1 == "lose" or r1 == "draw", "seat 1 multi battle finished (" .. tostring(r1) .. ")")
result(shots1.start and shots1.party, "seat 1 multi battle start and party menu were captured")
result(info1.own == 2, "seat 1 controls battler 2 (player right)")
local b1 = info1.backs or {}
result(b1.own == 1 and b1.partner == 1,
"seat 1 draws LEAF's and partner GREEN's female back pics (" .. tostring(b1.own) .. "," .. tostring(b1.partner) .. ")")
local b0 = info0.backs or {}
result(b0.own == 0 and b0.partner == 0,
"seat 0 draws RED's and partner BLUE's male back pics (" .. tostring(b0.own) .. "," .. tostring(b0.partner) .. ")")
local own1 = {}
for view, allied in pairs(info1.allies or {}) do if allied then own1[#own1 + 1] = view end end
table.sort(own1)
result(table.concat(own1, ",") == "2,5,6", "seat 1 party menu tints the ally's boxes 2, 5 and 6 (" .. table.concat(own1, ",") .. ")")
local relayS = H.relay({ seed = SEED, seats = 4, roomSeed = 30 })
local feeds = {}
for seat = 0, 3 do feeds[seat] = relayS:session(seat) end
for _, row in ipairs(relay0.log) do
if row.seat ~= nil and feeds[row.seat] and row.msg.type ~= "game3_bye" then feeds[row.seat]:send(row.msg) end
end
local watcher = relayS:session(nil)
returned = nil
local rS
game:enterArena({
profile = profile(), role = "spectator", seats = 4, seed = SEED, match = "ms", room = relayS.room,
players = players(), session = watcher, client = relayS:client(), mode = "multi",
onDone = function(r) rS = r end,
})
local takenS = {}
for _ = 1, 40000 do
if returned then break end
relayS:tick()
local st = Battle.getState()
local Message = package.loaded["src.ui.game3.message"]
local page = (Battle.isActive() and st and Message and Message.isOpen and Message.isOpen() and Message.isWaiting
and Message.isWaiting() and tostring(Message.currentPage() or "")) or ""
page = page:gsub("\n", " ")
if not takenS.intro and page:find("want to battle", 1, true) then
takenS.intro = true
U.still(game, DIR .. "/g3battle_multi_spectator_intro.png")
elseif not takenS.mid and st and (st.turn or 0) >= 2 and page:find(" used ", 1, true) then
takenS.mid = true
U.still(game, DIR .. "/g3battle_multi_spectator_mid_turn.png")
elseif not takenS.result and (page:find("defeated", 1, true) or page:find("lost to", 1, true)
or page:find("lost against", 1, true) or page:find("draw", 1, true)) then
takenS.result = true
U.still(game, DIR .. "/g3battle_multi_spectator_result.png")
end
U.wait(1)
end
result(rS == "ended", "the spectator watched the whole multi battle (" .. tostring(rS) .. ")")
result(takenS.intro and takenS.mid and takenS.result, "spectator intro, mid-turn and result were captured")
local spoke = false
for _, row in ipairs(relayS.log) do if row.seat == nil then spoke = true end end
result(not spoke, "the spectator never sent a message")
result(game.phase == "arena" and game.arena == nil, "the arena state was torn down")
finish()
end