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

657 lines
26 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
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
local Json = require("src.link.Json")
local Wire = require("src.link.Wire")
local MG = require("src.core.game3.minigames.common")
local Records = require("src.ui.game3.minigame_records")
local Countdown = require("src.ui.game3.minigames.common_countdown")
local ROOM = "r0123456789abcdef"
local Hub = {}
Hub.__index = Hub
function Hub.new(n)
local self = setmetatable({
order = {}, inbox = {}, present = {}, online = {}, leader = 0, epoch = 1,
log = {}, dropped = {}, names = {},
}, Hub)
for s = 0, n - 1 do
self.order[#self.order + 1] = s
self.inbox[s] = {}
self.present[s] = true
self.online[s] = true
self.names[s] = ({ "RED", "BLUE", "LEAF", "GOLD", "KRIS" })[s + 1]
end
return self
end
function Hub:playersList()
local out = {}
for _, s in ipairs(self.order) do
if self.present[s] then
out[#out + 1] = { id = ("%08x"):format(0xa0 + s), name = self.names[s], seat = s,
online = self.online[s], ready = true }
end
end
return out
end
local function wireCopy(msg)
local decoded = Json.decode(Json.encode(msg))
return Wire.sanitize(decoded)
end
function Hub:deliver(to, msg)
if not self.present[to] then return end
local q = self.inbox[to]
q[#q + 1] = wireCopy(msg)
q[#q].seat = msg.seat
q[#q].relay = msg.relay
end
function Hub:route(from, msg)
if not self.present[from] then return end
local inner = wireCopy(msg)
if not inner then
self.dropped[#self.dropped + 1] = { from = from, type = msg.type, why = "schema" }
return
end
if #Json.encode(msg) > ((msg.type == MG.MSG.RESULT) and 1024 or 512) then
self.dropped[#self.dropped + 1] = { from = from, type = msg.type, why = "cap" }
return
end
if inner.type == MG.MSG.STATE and from ~= self.leader then
self.dropped[#self.dropped + 1] = { from = from, type = msg.type, why = "not_leader" }
return
end
inner.seat = from
self.log[#self.log + 1] = { from = from, type = inner.type }
for _, s in ipairs(self.order) do
if s ~= from then self:deliver(s, inner) end
end
end
function Hub:migrate(prev)
local nextSeat
for _, s in ipairs(self.order) do
if s > prev and self.present[s] and self.online[s] then nextSeat = s break end
end
if nextSeat == nil then
for _, s in ipairs(self.order) do
if s ~= prev and self.present[s] and self.online[s] then nextSeat = s break end
end
end
self.leader = nextSeat
self.epoch = self.epoch + 1
local msg = { type = MG.MSG.LEADER, seat = nextSeat, prev = prev, epoch = self.epoch, relay = true }
for _, s in ipairs(self.order) do self:deliver(s, msg) end
end
function Hub:drop(seat)
self.present[seat] = false
self.inbox[seat] = {}
if seat == self.leader then self:migrate(seat) end
end
function Hub:session(seat)
local hub = self
local rs = { paired = true, closed = false, left = false, target = ROOM }
function rs:send(msg) hub:route(seat, msg) end
function rs:poll()
local q = hub.inbox[seat]
hub.inbox[seat] = {}
return q
end
function rs:players() return hub:playersList() end
function rs:close()
self.left = true
self.closed = true
hub:drop(seat)
end
return rs
end
function Hub:client(seat)
local hub = self
return {
state = function() return hub.present[seat] and "online" or "offline" end,
room = function()
if not hub.present[seat] then return nil end
return { room = ROOM, leader = hub.leader, players = hub:playersList() }
end,
}
end
local TestGame = { id = "test", MIN = 2, MAX = 5, END_FRAME = 300 }
TestGame.__index = TestGame
function TestGame.loadArt() return {} end
function TestGame.new(ctx)
local sim = setmetatable({
ctx = ctx, f = 0, scores = {}, last = {}, presses = 0, predicted = nil,
leader = ctx.leader, becameLeader = 0, applied = 0,
}, TestGame)
for i = 1, 5 do sim.scores[i] = 0 sim.last[i] = 0 end
return sim
end
function TestGame:leaderStep(inputs, present)
for s = 0, 4 do
local i = inputs[s + 1]
if present[s + 1] and type(i) == "number" and i > self.last[s + 1] then
self.scores[s + 1] = self.scores[s + 1] + (i - self.last[s + 1])
self.last[s + 1] = i
end
end
self.f = self.f + 1
end
function TestGame:snapshot()
return { f = self.f, sc = { unpack(self.scores) }, la = { unpack(self.last) } }
end
function TestGame:applySnapshot(s, f)
self.f = s.f
for i = 1, 5 do
self.scores[i] = s.sc[i] or 0
self.last[i] = s.la[i] or 0
end
self.applied = self.applied + 1
end
function TestGame:localInput(input)
if input and input:wasPressed("a") then self.presses = self.presses + 1 end
return self.presses
end
function TestGame:predict(i) self.predicted = i end
function TestGame:becomeLeader(last)
if last then self:applySnapshot(last, last.f) end
self.leader = true
self.becameLeader = self.becameLeader + 1
end
function TestGame:update() end
function TestGame:draw() end
function TestGame:finished() return self.f >= TestGame.END_FRAME end
function TestGame:results()
local results, powder = {}, {}
for _, p in ipairs(self.ctx.players) do
local sc = self.scores[p.seat + 1]
results[#results + 1] = { seat = p.seat, score = sc, stats = { presses = sc } }
powder[#powder + 1] = { seat = p.seat, amount = sc * 10 }
end
return { results = results, powder = powder }
end
function TestGame.applyResults(session, result, mySeat)
for _, r in ipairs(result.results) do
if r.seat == mySeat then Records.updatePokemonJump(session, r.score, r.score, 0) end
end
end
local function inputFor(schedule)
local input = { frame = 0 }
function input:wasPressed(b) return b == "a" and schedule(self.frame) end
function input:isDown() return false end
return input
end
local function makeSeats(n, opts)
opts = opts or {}
local hub = Hub.new(n)
local seats = {}
local players = {}
for s = 0, n - 1 do
players[#players + 1] = { id = ("%08x"):format(0xa0 + s), name = hub.names[s], seat = s,
trainerId = 0x1000 + s, gender = s % 2 }
end
for s = 0, n - 1 do
local session = { berryPowder = opts.powder or 0 }
local spec = { game = "jump", session = hub:session(s), seat = s, seats = n, players = players,
seed = 424242, partySlot = 0 }
local m = MG.Match.new(spec, {
module = opts.module or TestGame,
client = hub:client(s),
partyMon = { species = 4, level = 10 },
countdown = function() return Countdown.new(120, 80, { playSe = function() end }) end,
onResult = function(msg, match) MG.applyResults(session, msg, match.seat, opts.module or TestGame) end,
})
seats[s] = { match = m, session = session, input = inputFor(opts.schedule and opts.schedule(s)
or function(f) return f % (7 + s) == 0 end) }
end
return hub, seats
end
local function stepAll(seats, n, hook)
for _ = 1, n do
for s = 0, #seats do
local st = seats[s]
if st and not st.gone then
st.input.frame = st.input.frame + 1
st.match:step(st.input)
end
end
if hook and hook() then return end
end
end
local function allDone(seats)
for s = 0, #seats do
local st = seats[s]
if st and not st.gone and not st.match:finished() then return false end
end
return true
end
print("[test] 1. countdown timing matches minigame_countdown.c")
do
local c = Countdown.new(120, 80, { playSe = function() end })
local seen, order = {}, {}
local frames = 0
while c:step() do
frames = frames + 1
local d = c:digitShown()
if d and not seen[d] then seen[d] = true order[#order + 1] = d end
end
eq(table.concat(order, ","), "3,2,1", "digits count down 3, 2, 1")
eq(Countdown.totalFrames(), frames, "totalFrames is the run length")
check(frames > 200 and frames < 300, "about four seconds at 60 Hz (" .. frames .. ")")
local bounces = 0
local c2 = Countdown.new(120, 80, { playSe = function(id) if id == 50 then bounces = bounces + 1 end end })
while c2:step() do end
eq(bounces, 9, "SE_BALL_BOUNCE_2 per digit plus three per START half")
end
print("[test] 2. three seats: start, net loop, results agreement")
do
local hub, seats = makeSeats(3)
stepAll(seats, 3)
eq(seats[0].match.phase, "countdown", "leader started once every seat was ready")
eq(seats[1].match.phase, "countdown", "member 1 got game3_mg_start")
eq(seats[2].match.phase, "countdown", "member 2 got game3_mg_start")
local startPlayers = seats[2].match.startPlayers
eq(#startPlayers, 3, "start carries three players")
eq(startPlayers[2].species, 4, "each player's species comes from its game3_mg_ready")
eq(startPlayers[3].trainerId, 0x1002, "trainer ids ride game3_mg_start")
eq(seats[1].match.seed, 424242, "the relay seed is the game seed")
local stateSends, inputSends = 0, 0
local maxInputBurst = 0
local window = {}
stepAll(seats, 2000, function()
return allDone(seats)
end)
for _, e in ipairs(hub.log) do
if e.type == MG.MSG.STATE then stateSends = stateSends + 1 end
if e.type == MG.MSG.INPUT then inputSends = inputSends + 1 end
end
eq(seats[0].match.phase, "done", "leader done")
eq(seats[1].match.phase, "done", "member 1 done")
eq(seats[2].match.phase, "done", "member 2 done")
eq(stateSends, math.floor(TestGame.END_FRAME / MG.STATE_EVERY), "one state every 3rd leader frame")
check(inputSends > 0, "members sent inputs")
local r0 = Json.encode(seats[0].match.result.results)
eq(Json.encode(seats[1].match.result.results), r0, "member 1 results equal the leader's")
eq(Json.encode(seats[2].match.result.results), r0, "member 2 results equal the leader's")
local res = seats[0].match.result.results
for s = 0, 2 do
local sim = seats[s].match.sim
local made = seats[s].match.lastLocal
check(res[s + 1].score <= made and res[s + 1].score >= made - 1,
"seat " .. s .. " score counts its presses up to the final frames (" .. res[s + 1].score .. "/" .. made .. ")")
check(res[s + 1].score > 0, "seat " .. s .. " scored")
eq(seats[s].session.berryPowder, res[s + 1].score * 10, "seat " .. s .. " got its own Berry Powder")
eq(seats[s].session.pokemonJumpRecords.bestJumpScore, res[s + 1].score, "seat " .. s .. " record written")
check(sim.applied > 0 or s == 0, "seat " .. s .. " rendered from leader states")
end
local byes = 0
for _, e in ipairs(hub.log) do if e.type == MG.MSG.BYE then byes = byes + 1 end end
eq(byes, 3, "every seat said game3_mg_bye")
eq(#hub.dropped, 0, "nothing the relay would drop")
end
print("[test] 3. member inputs are coalesced to <= 20/s")
do
local hub, seats = makeSeats(2, { schedule = function(s)
if s == 1 then return function() return true end end
return function() return false end
end })
stepAll(seats, 3 + Countdown.totalFrames() + 60)
local sends = {}
for _, e in ipairs(hub.log) do
if e.type == MG.MSG.INPUT then sends[#sends + 1] = e end
end
check(#sends <= 21 and #sends >= 18, "60 frames of mashing sent about 20 inputs (" .. #sends .. ")")
local m = seats[1].match
eq(m.sim.predicted, m.lastLocal, "own input is predicted immediately")
end
print("[test] 4. leader inputs apply through the 2-frame buffer")
do
local hub, seats = makeSeats(2, { schedule = function() return function() return false end end })
stepAll(seats, 3 + Countdown.totalFrames() + 1)
local leader = seats[0].match
eq(leader.phase, "play", "leader in play")
local f0 = leader.f
leader:handle({ type = MG.MSG.INPUT, seat = 1, f = 10, i = 5 })
leader:step(seats[0].input)
eq(leader.sim.scores[2], 0, "not applied on the arrival frame")
leader:step(seats[0].input)
eq(leader.sim.scores[2], 0, "not applied one frame later")
leader:step(seats[0].input)
eq(leader.sim.scores[2], 5, "applied two frames after arrival")
check(leader.f == f0 + 3, "three leader frames ran")
end
print("[test] 5. leader drop mid-game migrates leadership to the next seat")
do
local hub, seats = makeSeats(3)
local dropAt = 3 + Countdown.totalFrames() + 120
local frame = 0
local before
stepAll(seats, dropAt, function()
frame = frame + 1
end)
eq(seats[0].match.phase, "play", "game running")
before = { seats[1].match.sim.scores[2], seats[2].match.sim.scores[3] }
seats[0].gone = true
hub:drop(0)
eq(hub.leader, 1, "relay picked seat 1")
stepAll(seats, 1)
check(seats[1].match:isLeader(), "seat 1 is the leader now")
eq(seats[2].match.leader, 1, "seat 2 follows seat 1")
eq(seats[1].match.sim.becameLeader, 1, "sim:becomeLeader ran once on seat 1")
eq(seats[1].match.epoch, seats[2].match.epoch, "both seats agree on the epoch")
check(seats[1].match.epoch >= 2, "epoch advanced")
local stale = { type = MG.MSG.STATE, seat = 0, f = 99999, e = 1, s = { f = 1, sc = { 999, 999, 999, 0, 0 }, la = {} } }
seats[2].match:handle(stale)
check(seats[2].match.sim.scores[1] ~= 999, "a state from the old epoch is ignored")
stepAll(seats, 2000, function() return allDone(seats) end)
eq(seats[1].match.phase, "done", "new leader finished")
eq(seats[2].match.phase, "done", "member finished")
local r1 = Json.encode(seats[1].match.result.results)
eq(Json.encode(seats[2].match.result.results), r1, "survivors agree on the results")
local res = seats[1].match.result.results
eq(#res, 3, "results still name all three start seats")
check(res[2].score >= before[1], "seat 1 kept its score across the migration")
check(res[3].score >= before[2], "seat 2 kept its score across the migration")
check(res[3].score >= seats[2].match.lastLocal - 1, "seat 2's presses after the migration counted")
eq(seats[0].session.berryPowder, 0, "the dropped leader wrote no records")
check(seats[2].session.berryPowder > 0, "survivor got Berry Powder")
local notLeader = 0
for _, d in ipairs(hub.dropped) do if d.why == "not_leader" then notLeader = notLeader + 1 end end
eq(notLeader, 0, "no member ever sent a state")
end
print("[test] 6. dropping below MIN is a communication error with no records")
do
local hub, seats = makeSeats(2)
stepAll(seats, 3 + Countdown.totalFrames() + 30)
seats[1].gone = true
hub:drop(1)
stepAll(seats, 2)
eq(seats[0].match.phase, "error", "leader errors out")
eq(seats[0].match.why, "dropped", "because too few seats remain")
eq(seats[0].session.berryPowder, 0, "no Berry Powder")
eq(seats[0].session.pokemonJumpRecords, nil, "no records")
end
print("[test] 7. leader drop during the ready handshake")
do
local hub, seats = makeSeats(3)
seats[0].gone = true
hub:drop(0)
stepAll(seats, 3)
eq(seats[1].match.leader, 1, "seat 1 leads")
eq(seats[1].match.phase, "countdown", "seat 1 started the game")
eq(seats[2].match.phase, "countdown", "seat 2 got the start")
eq(#seats[2].match.startPlayers, 2, "only the two present seats play")
end
print("[test] 8. a resumed old leader becomes a member")
do
local hub, seats = makeSeats(3)
stepAll(seats, 3 + Countdown.totalFrames() + 10)
local m0 = seats[0].match
m0.sim.becomeMember = function(self) self.member = true end
m0:handle({ type = MG.MSG.LEADER, seat = 1, prev = 0, epoch = 2, relay = true })
check(not m0:isLeader(), "seat 0 stepped down")
check(m0.sim.member, "sim:becomeMember ran")
end
print("[test] 8b. game3_mg_ready names beat the spec's relay names")
do
local hub, seats = makeSeats(2)
local m = seats[0].match
m.me = { name = "RED", trainerId = 0x1234, gender = 0 }
local ready = m:readyPayload()
eq(ready.name, "RED", "ready carries the cart name")
eq(ready.trainerId, 0x1234, "and the trainer id")
m.ready[1] = { species = 85, name = "LEAF", trainerId = 77, gender = 1 }
local list = m:startPlayersList({ 1 })
eq(list[1].name, "LEAF", "start uses the ready name")
eq(list[1].trainerId, 77, "and its trainer id")
eq(list[1].gender, 1, "and gender")
m.ready[1] = { species = 85 }
list = m:startPlayersList({ 1 })
eq(list[1].name, "BLUE", "without one the spec name stands")
end
print("[test] 9. relay limits: snapshot and result fit the caps")
do
local sim = TestGame.new({ players = {}, leader = true })
local bytes = #Json.encode({ type = MG.MSG.STATE, f = 99999, e = 3, s = sim:snapshot() })
check(bytes <= 512, "state packet " .. bytes .. " bytes <= 512")
end
print("[test] 10. records and Berry Powder")
do
local s = { berryPowder = 99990 }
eq(Records.giveBerryPowder(s, 5), true, "under the cap")
eq(Records.giveBerryPowder(s, 50), false, "over the cap reports FALSE")
eq(s.berryPowder, 99999, "Berry Powder caps at 99999")
local t = {}
eq(Records.updateBerryCrush(t, 3, 0x0280), true, "3-player speed recorded")
eq(t.berryCrushPressingSpeeds[2], 0x0280, "slot 2 holds 3 players")
eq(Records.updateBerryCrush(t, 3, 0x0100), false, "slower is not a record")
eq(Records.updatePokemonJump(t, 100000, 12, 3), true, "jumps in a row is a record")
eq(t.pokemonJumpRecords.bestJumpScore, 0, "a score over 99990 is not stored")
eq(t.pokemonJumpRecords.jumpsInRow, 12, "jumps in a row stored")
Records.incrementPokemonJumpMaxPlayerGames(t)
eq(t.pokemonJumpRecords.gamesWithMaxPlayers, 1, "max player games counted")
eq(Records.updateDodrio(t, 2000000, 20000, 7), true, "dodrio record")
eq(t.dodrioBerryPickingRecords.bestScore, 999990, "score clamps to 999990")
eq(t.dodrioBerryPickingRecords.berriesPicked, 9999, "berries clamp to 9999")
eq(t.dodrioBerryPickingRecords.berriesPickedInRow, 7, "berries in a row")
local session = { berryPowder = 0 }
MG.applyResults(session, { results = {}, powder = { { seat = 1, amount = 30 }, { seat = 2, amount = 7 } } }, 2, nil)
eq(session.berryPowder, 7, "applyResults gives only my seat's powder")
end
print("[test] 11. MG.arm launches on the waitstate and returns after exit")
do
local ctx = {}
local hub = Hub.new(2)
package.loaded["src.online.Client"] = hub:client(0)
local exits = {}
local t = 0
local realClock = MG.clock
MG.clock = function() return t end
local realJump = package.loaded[MG.GAMES.jump]
package.loaded[MG.GAMES.jump] = { MIN = 2, MAX = 5, new = function() return {} end,
loadArt = function() return nil, "pokemon_jump/manifest.lua is missing from the cache" end }
local rs = hub:session(0)
local spec = { game = "jump", session = rs, seat = 0, seats = 2,
players = { { seat = 0, name = "RED" }, { seat = 1, name = "BLUE" } },
seed = 7, returnToMap = false, onExit = function(r) exits[#exits + 1] = r end }
eq(MG.arm(ctx, spec), true, "arm returns true")
check(type(ctx.stateWait) == "function", "arm waits on the script's waitstate")
eq(MG.isActive(), false, "nothing runs before the waitstate")
local poll = ctx.stateWait
eq(poll(), false, "first poll launches")
check(MG.isActive(), "minigame running")
for _ = 1, 600 do
t = t + 1 / 60
MG.update(1 / 60)
if not MG.isActive() then break end
end
eq(MG.isActive(), false, "missing game art ends the run")
eq(exits[1], "error", "onExit(error)")
check(rs.left and rs.closed, "a run with no Match still leaves the relay room")
eq(poll(), true, "the waitstate releases after exit")
MG.clock = realClock
package.loaded[MG.GAMES.jump] = realJump
package.loaded["src.online.Client"] = nil
end
print("[test] 12. ChooseMonForWirelessMinigame and IsPokemonJumpSpeciesInParty")
do
local vars = {}
local session = { party = { { species = 85, level = 30 }, { species = 4, level = 5 },
{ species = 4, level = 5, isEgg = true } } }
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
package.loaded["src.core.game3.scripting.flags"] = {
getVar = function(_, _, id) return vars[id] or 0 end,
setVar = function(_, _, id, v) vars[id] = v end,
}
local shown = {}
local PM = { cursor = 1, mode = "choose" }
function PM.show(party, _, opts) shown[#shown + 1] = opts PM.opts = opts PM.mode = "choose" end
function PM.close() local cb = PM.opts.onClose if cb then cb() end end
function PM.showYesNo(text, cb) PM.mode = "yesno" PM.yes = cb PM.prompt = text end
package.loaded["src.ui.game3.party_menu"] = PM
package.loaded["src.core.game3.rom_text"] = { box = function(k) return k end, plain = function(k) return k end }
MG._jumpMons = { [4] = 1, [7] = 0 }
package.loaded["src.core.game3.scripting.natives_wireless"] = nil
local Std = require("src.core.game3.scripting.stdscripts")
local Wireless = require("src.core.game3.scripting.natives_wireless")
local ctx = {}
vars[0x8005] = 0
Wireless.HANDLERS[Std.SPECIAL.IsPokemonJumpSpeciesInParty](ctx)
eq(vars[0x800D], 1, "Charmander can jump")
session.party[2].species = 85
Wireless.HANDLERS[Std.SPECIAL.IsPokemonJumpSpeciesInParty](ctx)
eq(vars[0x800D], 0, "an egg and two Dodrio cannot")
session.party[2].species = 4
Wireless.HANDLERS[Std.SPECIAL.ChooseMonForWirelessMinigame](ctx)
check(type(ctx.stateWait) == "function", "party menu waits on the waitstate")
eq(PM.opts.mode, "choose", "cart party menu opened")
check(PM.opts.validate(1) ~= nil, "Dodrio cannot enter Pokemon Jump")
check(PM.opts.validate(3) ~= nil, "an egg cannot enter")
eq(PM.opts.validate(2), nil, "Charmander can")
eq(PM.opts.minigameEligible(2, session.party[2]), true, "ABLE marker for Charmander")
PM.close()
PM.opts.onSelect(2)
eq(ctx.stateWait(), true, "waitstate released once the menu closed with a pick")
eq(vars[0x8004], 1, "VAR_0x8004 = the 0-based slot")
eq(MG.partySlot, 1, "MG remembers the slot for the group spec")
vars[0x8005] = 1
ctx = {}
Wireless.HANDLERS[Std.SPECIAL.ChooseMonForWirelessMinigame](ctx)
eq(PM.opts.validate(1), nil, "Dodrio can pick berries")
check(PM.opts.validate(2) ~= nil, "Charmander cannot")
PM.close()
eq(ctx.stateWait(), false, "B does not release the waitstate")
eq(PM.mode, "yesno", "B asks to cancel participation")
eq(PM.prompt, "gText_CancelParticipation", "with the cart's text")
PM.yes(false)
eq(PM.mode, "choose", "NO goes back to the list")
eq(ctx.stateWait(), false, "still waiting")
PM.close()
eq(ctx.stateWait(), false, "asked again")
PM.yes(true)
eq(ctx.stateWait(), true, "YES releases the waitstate")
eq(vars[0x8004], 6, "YES cancels with PARTY_SIZE")
eq(MG.partySlot, nil, "no slot remembered")
package.loaded["src.core.game3.runtime"] = nil
package.loaded["src.core.game3.scripting.flags"] = nil
package.loaded["src.ui.game3.party_menu"] = nil
package.loaded["src.core.game3.rom_text"] = nil
package.loaded["src.core.game3.scripting.natives_wireless"] = nil
MG._jumpMons = nil
end
print("[test] 13. ROM tables and cart text (needs the cache)")
do
local Cache = require("tests.game3_cache")
local bundle = Cache.bundle("pokemon_jump/tables.lua")
if not bundle then
print("[skip] cache-backed checks: " .. tostring(Cache.reason))
else
MG._jumpMons = nil
check(MG.isJumpSpecies(4), "Charmander is in sPokeJumpMons")
check(not MG.isJumpSpecies(85), "Dodrio is not")
local n = 0
for _ in pairs(MG.jumpMons()) do n = n + 1 end
eq(n, 100, "100 jump species from the ROM")
local Art = require("src.ui.game3.minigames.common_art")
local art, err = Countdown.loadArt(Art.cache())
check(art ~= nil, "countdown art loads from link/ (" .. tostring(err) .. ")")
if art then
eq(art.minigame_countdown_numbers.frames, 3, "three digits")
eq(art.minigame_countdown_start.frame_w, 64, "START halves are 64 wide")
end
local Lobby = require("src.ui.game3.minigames.common_lobby")
Lobby.showPlayers({}, { mode = "leader", capacity = { min = 1, max = 4 }, activity = 9 })
eq(Lobby.group.min, 2, "leader capacity from members + 1")
check(Lobby.modeText():find("1 player") ~= nil, "alone: 1 player needed (" .. Lobby.modeText() .. ")")
check(Lobby.awaitingText():find("Awaiting") ~= nil, "awaiting communication")
check(not Lobby.canStart(), "START needs another player")
Lobby.showPlayers({ { name = "BLUE", trainerId = 1 } }, { mode = "leader", group = { min = 2, max = 5 }, activity = 9 })
check(Lobby.modeText():find("2%-PLAYER") ~= nil, "two players: 2-PLAYER MODE (" .. Lobby.modeText() .. ")")
check(Lobby.awaitingText():find("START") ~= nil, "press START when everyone's ready")
check(Lobby.awaitingText():find(Lobby.activityName(), 1, true) ~= nil, "names the activity")
check(Lobby.canStart(), "START allowed")
local started = false
Lobby._onConfirm = function() started = true end
check(Lobby.confirm(), "confirm")
check(started, "leader START fires onConfirm")
Lobby.showPlayers({}, { mode = "leader", group = { min = 3, max = 5 }, activity = 11 })
check(Lobby.modeText():find("2 players") ~= nil, "Dodrio alone: 2 players needed")
local okMsg, Message = pcall(require, "src.ui.game3.message")
if okMsg then
Message.show("Please decide which of you will become the GROUP LEADER.", { stay = true })
check(Message.isOpen(), "the script's leader prompt is up")
end
Lobby.showPlayers({ { slot = 1, name = "LEAF", started = true }, { slot = 2, name = "GOLD" } },
{ mode = "group", capacity = { activity = 10, min = 2, max = 5 } })
check(Lobby.chooseText() ~= "", "joiner prompt from gTexts_UR_ChooseTrainer")
if okMsg then check(not Message.isOpen(), "the group list reprints the textbox (union_room.c:1168)") end
check(not Lobby.confirm(), "a started group plays SE_WALL_HIT")
Lobby.move(1)
local joined
Lobby._onConfirm = function(slot) joined = slot end
check(Lobby.confirm(), "join the open group")
eq(joined, 2, "slot passed back")
Lobby.reset()
end
end
if failed > 0 then
print(("[FAIL] %d check(s) failed"):format(failed))
os.exit(1)
end
print("[PASS] game3_minigame_common_test")