mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
642 lines
24 KiB
Lua
642 lines
24 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 function finish()
|
|
if failed > 0 then
|
|
print(string.format("[FAIL] game3_mg_pokemon_jump_test: %d failure(s)", failed))
|
|
os.exit(1)
|
|
end
|
|
print("[PASS] game3_mg_pokemon_jump_test")
|
|
os.exit(0)
|
|
end
|
|
|
|
local Game = require("src.core.game3.minigames.pokemon_jump.game")
|
|
local Gfx = require("src.core.game3.minigames.pokemon_jump.gfx")
|
|
|
|
print("[test] 1. cart arithmetic (ROM-free)")
|
|
do
|
|
eq(Game.isoRandomize1(0), 24691, "ISO_RANDOMIZE1(0)")
|
|
eq(Game.isoRandomize1(24691), 3917380458, "ISO_RANDOMIZE1 is exact u32 math")
|
|
eq(Game.isoRandomize1(0xFFFFFFFF), 3191476742, "ISO_RANDOMIZE1 wraps at 2^32")
|
|
local item, qty = Game.unpackPrizeData(2 * 0x1000 + 141)
|
|
eq(item, 141, "prize data item in the low 12 bits")
|
|
eq(qty, 2, "prize quantity in the top 4 bits")
|
|
eq(Game.prizeItemName(141, 2, "LUM BERRY"), "LUM BERRIES", "two berries use sPluralTxt")
|
|
eq(Game.prizeItemName(141, 1, "LUM BERRY"), "LUM BERRY", "one berry stays singular")
|
|
eq(Game.prizeItemName(175, 3, "ENIGMA BERRY"), "ENIGMA BERRY", "LAST_BERRY_INDEX is excluded")
|
|
end
|
|
|
|
local Cache = require("tests.game3_cache")
|
|
local root = Cache.mount("pokemon_jump/tables.lua")
|
|
if not root then
|
|
print("[skip] cache-backed checks: " .. tostring(Cache.reason))
|
|
finish()
|
|
end
|
|
print("[info] FireRed cache at " .. root)
|
|
|
|
local Json = require("src.link.Json")
|
|
local Wire = require("src.link.Wire")
|
|
local MG = require("src.core.game3.minigames.common")
|
|
local Countdown = require("src.ui.game3.minigames.common_countdown")
|
|
local Records = require("src.ui.game3.minigame_records")
|
|
local G = require(MG.GAMES.jump)
|
|
local Art = require("src.ui.game3.minigames.common_art")
|
|
|
|
local art, artErr = G.loadArt(Art.cache())
|
|
check(art ~= nil, "Pokemon Jump art loads from the cache (" .. tostring(artErr) .. ")")
|
|
if not art then finish() end
|
|
local tables = art.tables
|
|
|
|
local F = Game.FUNC
|
|
local VINE = Game.VINE
|
|
local MON = Game.MONSTATE
|
|
|
|
print("[test] 2. ROM tables drive the rules")
|
|
do
|
|
eq(#tables.jump_mons, 100, "sPokeJumpMons has 100 species")
|
|
eq(#tables.vine_base_speeds, 8, "sVineBaseSpeeds")
|
|
eq(tables.score_bonuses[3], 50, "two players in sync score 50")
|
|
eq(tables.score_bonuses[6], 500, "five players in sync score 500")
|
|
eq(tables.prize_quantity[1].score, 5000, "prizes start at 5000 points")
|
|
eq(#tables.prize_items, 8, "eight prize berries")
|
|
eq(G.MIN, 2, "two players minimum")
|
|
eq(G.MAX, 5, "five players maximum")
|
|
eq(G.DROPPED_TEXT, "gText_SomeoneDroppedOut2", "drop text is the cart's")
|
|
check(art.interface[2] ~= nil and art.interface[4] ~= nil, "interface palette (name colors) from bg.pal")
|
|
eq(art.digits.frames, 11, "minigame digits sheet")
|
|
eq(art.vine2.frame_h, 32, "vine2 is the tall vine")
|
|
end
|
|
|
|
local function newHooks(log)
|
|
log = log or {}
|
|
local h = { log = log, bag = {}, full = false, limit = nil }
|
|
h.hooks = {
|
|
playSe = function(id) log[#log + 1] = { "se", id } end,
|
|
playFanfare = function(id) log[#log + 1] = { "fanfare", id } end,
|
|
fanfareDone = function() return true end,
|
|
playMusic = function(id) log[#log + 1] = { "music", id } end,
|
|
canAdd = function(item, qty)
|
|
if h.full then return false end
|
|
if h.limit then return ((h.bag[item] or 0) + qty) <= h.limit end
|
|
return true
|
|
end,
|
|
addItem = function(item, qty)
|
|
if not h.hooks.canAdd(item, qty) then return false end
|
|
h.bag[item] = (h.bag[item] or 0) + qty
|
|
log[#log + 1] = { "item", item, qty }
|
|
return true
|
|
end,
|
|
updateRecords = function(score, row, exc)
|
|
log[#log + 1] = { "records", score, row, exc }
|
|
h.records = { score, row, exc }
|
|
return true
|
|
end,
|
|
incrementMaxPlayerGames = function() log[#log + 1] = { "maxgames" } end,
|
|
save = function() log[#log + 1] = { "save" } end,
|
|
}
|
|
return h
|
|
end
|
|
|
|
local function countLog(log, kind, a)
|
|
local n = 0
|
|
for _, e in ipairs(log) do
|
|
if e[1] == kind and (a == nil or e[2] == a) then n = n + 1 end
|
|
end
|
|
return n
|
|
end
|
|
|
|
local function standalone(opts)
|
|
opts = opts or {}
|
|
local h = newHooks()
|
|
local players = {}
|
|
for s = 0, (opts.n or 2) - 1 do
|
|
players[#players + 1] = { seat = s, name = ({ "RED", "BLUE", "LEAF", "GOLD", "KRIS" })[s + 1], species = 4 }
|
|
end
|
|
local sim = G.new({
|
|
game = "jump", seat = opts.seat or 0, seats = #players, players = players, leader = (opts.seat or 0) == 0,
|
|
seed = 1234, rng = MG.rng(1234), partyMon = { species = 4, personality = 0x12345678, otId = 1 },
|
|
art = art, hooks = h.hooks,
|
|
countdown = function() return Countdown.new(120, 80, { playSe = function() end }) end,
|
|
})
|
|
return sim, h
|
|
end
|
|
|
|
print("[test] 3. vine speeds and swing timing (sVineBaseSpeeds, sVineSpeedDelays, PokeJumpRandom)")
|
|
do
|
|
local sim = standalone()
|
|
local g = sim.game
|
|
g.rngSeed = 0x1234
|
|
g:resetVineState()
|
|
eq(g.vineSpeed, 26, "first swing runs at sVineBaseSpeeds[0]")
|
|
eq(g.vineState, VINE.UPSWING_LOW, "ResetVineState leaves the vine on the upswing")
|
|
check(g.vineSpeedDelay >= 2 and g.vineSpeedDelay <= 4, "speed delay is sVineSpeedDelays[r % 4] + 2 (" .. g.vineSpeedDelay .. ")")
|
|
g:enableVineUpdates()
|
|
local swings, frames, speeds = 0, 0, {}
|
|
local lastSpeed = g.vineSpeed
|
|
local seen = {}
|
|
while swings < 40 and frames < 20000 do
|
|
g:updateVineState()
|
|
frames = frames + 1
|
|
seen[g.vineState] = true
|
|
if g.ignoreJumpInput ~= 0 then
|
|
g.ignoreJumpInput = 0
|
|
swings = swings + 1
|
|
speeds[#speeds + 1] = g.vineSpeed
|
|
end
|
|
lastSpeed = g.vineSpeed
|
|
end
|
|
eq(swings, 40, "forty swings")
|
|
local all = true
|
|
for s = 0, 9 do all = all and seen[s] == true end
|
|
check(all, "the vine passes through all ten states")
|
|
local stageUp = false
|
|
for _, sp in ipairs(speeds) do
|
|
if sp >= 26 + 7 then stageUp = true end
|
|
end
|
|
check(stageUp, "speed stage rises after the base table is walked (+7 per stage)")
|
|
local okRange = true
|
|
for _, sp in ipairs(speeds) do
|
|
if sp < 21 or sp > 82 then okRange = false end
|
|
end
|
|
check(okRange, "speeds stay inside the cart's range")
|
|
local g2 = standalone().game
|
|
g2.rngSeed = 0x1234
|
|
g2:resetVineState()
|
|
g2:enableVineUpdates()
|
|
local speeds2 = {}
|
|
local n = 0
|
|
while #speeds2 < 40 and n < 20000 do
|
|
g2:updateVineState()
|
|
n = n + 1
|
|
if g2.ignoreJumpInput ~= 0 then
|
|
g2.ignoreJumpInput = 0
|
|
speeds2[#speeds2 + 1] = g2.vineSpeed
|
|
end
|
|
end
|
|
eq(table.concat(speeds2, ","), table.concat(speeds, ","), "same seed, same vine (members share the leader's seed)")
|
|
end
|
|
|
|
print("[test] 4. jump arc, hit shake, intro bounce")
|
|
do
|
|
local sim = standalone()
|
|
local g = sim.game
|
|
local gfx = g.gfx
|
|
local row = tables.jump_offsets[1]
|
|
g.vineTimer = 100
|
|
g.player.monJumpType = 0
|
|
g.player.jumpTimeStart = 100
|
|
g.player.monState = MON.JUMP
|
|
g.player.prevMonState = MON.NORMAL
|
|
local ys, peak = {}, 0
|
|
for _ = 1, 60 do
|
|
g:handleMonState()
|
|
ys[#ys + 1] = gfx.monSprites[0].y2
|
|
if g.player.jumpOffset == -30 then peak = peak + 1 end
|
|
g.vineTimer = g.vineTimer + 1
|
|
if g.player.monState == MON.NORMAL then break end
|
|
end
|
|
eq(ys[1], 0, "four frames on the ground before the jump leaves")
|
|
eq(ys[5], row[1], "then sJumpOffsets[NORMAL][0]")
|
|
eq(peak, 3, "three frames at JUMP_PEAK for a normal jumper")
|
|
eq(g.player.monState, MON.NORMAL, "the mon lands and goes back to MONSTATE_NORMAL")
|
|
gfx:startMonHitShake(1)
|
|
local frames = 0
|
|
while gfx:isMonHitShakeActive(1) do
|
|
gfx:animateSprites()
|
|
frames = frames + 1
|
|
end
|
|
eq(frames, 26, "SpriteCB_MonHitShake runs 13 shakes two frames apart")
|
|
eq(gfx.monSprites[1].y2, 0, "shake ends level")
|
|
gfx:startMonIntroBounce(0)
|
|
local bounce, minY = 0, 0
|
|
while gfx:isMonIntroBounceActive() do
|
|
gfx:animateSprites()
|
|
bounce = bounce + 1
|
|
minY = math.min(minY, gfx.monSprites[0].y2)
|
|
end
|
|
eq(bounce, 64, "two 32-frame hops")
|
|
eq(minY, -32, "hop height is gSineTable >> 3")
|
|
gfx:doSameJumpTimeBonus(1 + 2)
|
|
eq(gfx.bonus.id, 0, "two players in sync show the first bonus plate")
|
|
eq(gfx:bonusScrollY(), -40, "plate starts 40 px down")
|
|
local visible = 0
|
|
for _ = 1, 40 do
|
|
gfx:update()
|
|
if not gfx.starSprites[0].invisible then visible = visible + 1 end
|
|
end
|
|
eq(visible, 37, "stars spin through sAnim_Star_Spinning then hide")
|
|
check(not gfx.bonus.visible, "bonus plate hides after 32 frames")
|
|
end
|
|
|
|
local ROOM = "r0123456789abcdef"
|
|
|
|
local Hub = {}
|
|
Hub.__index = Hub
|
|
|
|
function Hub.new(n)
|
|
local self = setmetatable({ order = {}, inbox = {}, present = {}, leader = 0, epoch = 1,
|
|
log = {}, dropped = {}, names = {}, maxState = 0, maxInput = 0, maxResult = 0 }, Hub)
|
|
for s = 0, n - 1 do
|
|
self.order[#self.order + 1] = s
|
|
self.inbox[s] = {}
|
|
self.present[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 = true, ready = true }
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function wireCopy(msg)
|
|
return Wire.sanitize(Json.decode(Json.encode(msg)))
|
|
end
|
|
|
|
function Hub:deliver(to, msg)
|
|
if not self.present[to] then return end
|
|
local q = self.inbox[to]
|
|
local c = wireCopy(msg)
|
|
c.seat = msg.seat
|
|
c.relay = msg.relay
|
|
q[#q + 1] = c
|
|
end
|
|
|
|
function Hub:route(from, msg)
|
|
if not self.present[from] then return end
|
|
local bytes = #Json.encode(msg)
|
|
local cap = (msg.type == MG.MSG.RESULT) and 1024 or 512
|
|
if msg.type == MG.MSG.STATE then self.maxState = math.max(self.maxState, bytes) end
|
|
if msg.type == MG.MSG.INPUT then self.maxInput = math.max(self.maxInput, bytes) end
|
|
if msg.type == MG.MSG.RESULT then self.maxResult = math.max(self.maxResult, bytes) end
|
|
if bytes > cap then
|
|
self.dropped[#self.dropped + 1] = { from = from, type = msg.type, why = "cap" }
|
|
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 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] then nextSeat = s break 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 SPECIES = { 4, 25, 1, 7, 133 }
|
|
|
|
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
|
|
G.hooksFor = function(ctx) return seats[ctx.seat].h.hooks end
|
|
for s = 0, n - 1 do
|
|
local st = { seat = s, h = newHooks(), session = {}, jump = true, answer = "yes", pressAt = 4 }
|
|
seats[s] = st
|
|
local spec = { game = "jump", session = hub:session(s), seat = s, seats = n, players = players,
|
|
seed = 97531, partySlot = 0 }
|
|
local input = { want = nil }
|
|
function input:wasPressed(b) return self.want == b end
|
|
function input:isDown() return false end
|
|
st.input = input
|
|
st.match = MG.Match.new(spec, {
|
|
module = G,
|
|
client = hub:client(s),
|
|
art = art,
|
|
partyMon = { species = SPECIES[s + 1], personality = 0x1000 * (s + 1), otId = 0x2000 + s },
|
|
me = { name = hub.names[s], trainerId = 0x1000 + s, gender = s % 2 },
|
|
countdown = function() return Countdown.new(120, 80, { playSe = function() end }) end,
|
|
onResult = function(msg, match) MG.applyResults(st.session, msg, match.seat, G) end,
|
|
})
|
|
end
|
|
return hub, seats
|
|
end
|
|
|
|
local function plan(st)
|
|
local input = st.input
|
|
input.want = nil
|
|
local sim = st.match.sim
|
|
if not sim then return end
|
|
local g = sim.game
|
|
local gfx = g.gfx
|
|
if gfx.yesno then
|
|
if st.answer == "yes" then
|
|
if gfx.yesno.cursor == 0 then input.want = "a" else input.want = "up" end
|
|
elseif st.answer == "no" then
|
|
if gfx.yesno.cursor == 1 then input.want = "a" else input.want = "down" end
|
|
elseif st.answer == "b" then
|
|
input.want = "b"
|
|
end
|
|
return
|
|
end
|
|
if gfx.msgWindow and gfx.msgWindow.itemId and gfx.msgWindow.shown and st.dismiss then
|
|
input.want = "a"
|
|
return
|
|
end
|
|
if st.jump and g.comm.funcId == F.GAME_ROUND and g.vineState == st.pressAt
|
|
and g.player.monState == MON.NORMAL then
|
|
input.want = "a"
|
|
end
|
|
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 and not st.match:finished() then
|
|
plan(st)
|
|
st.match:step(st.input)
|
|
end
|
|
end
|
|
if hook and hook() then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
print("[test] 5. three seats over the fake relay: intro, rounds, sync bonus, leader drop -> seat 1 leads")
|
|
do
|
|
local hub, seats = makeSeats(3)
|
|
local sawCountdown, sawNames, sawHighlight = false, false, false
|
|
stepAll(seats, 2000, function()
|
|
local sim = seats[1].match.sim
|
|
if sim then
|
|
local gfx = sim.game.gfx
|
|
if gfx.countdown and gfx.countdown:running() or seats[1].match.phase == "countdown" then sawCountdown = true end
|
|
if gfx.names.visible then
|
|
sawNames = true
|
|
if gfx.names.highlight then sawHighlight = true end
|
|
end
|
|
end
|
|
return seats[0].match.sim and seats[0].match.sim.game.comm.funcId == F.GAME_ROUND
|
|
end)
|
|
local m0, m1, m2 = seats[0].match, seats[1].match, seats[2].match
|
|
eq(m0.phase, "play", "leader playing")
|
|
check(sawNames and sawHighlight, "intro prints every name with our own highlighted")
|
|
check(sawCountdown, "one minigame countdown before the first swing")
|
|
eq(m0.sim.game.comm.funcId, F.GAME_ROUND, "leader reached FUNC_GAME_ROUND")
|
|
eq(countLog(seats[1].h.log, "music", Game.MUS_POKE_JUMP), 1, "MUS_POKE_JUMP starts")
|
|
local bonus, maxRow = false, 0
|
|
stepAll(seats, 20000, function()
|
|
local g = m0.sim.game
|
|
if g.comm.jumpScore > g.comm.jumpsInRow * 10 then bonus = true end
|
|
maxRow = math.max(maxRow, g.comm.jumpsInRow)
|
|
return g.comm.jumpsInRow >= 6
|
|
end)
|
|
local g0 = m0.sim.game
|
|
check(g0.comm.jumpsInRow >= 6, "six clean jumps in a row (" .. g0.comm.jumpsInRow .. ")")
|
|
check(bonus, "players jumping together earned a same-time bonus (score " .. g0.comm.jumpScore .. ")")
|
|
stepAll(seats, 10, function() return m2.sim.game.comm.jumpScore == g0.comm.jumpScore end)
|
|
eq(m2.sim.game.comm.jumpScore, g0.comm.jumpScore, "member score follows the leader")
|
|
check(countLog(seats[2].h.log, "se", Game.SE_LEDGE) > 0, "SE_LEDGE on jumps")
|
|
local se = tables.sound_effects
|
|
check(countLog(seats[2].h.log, "se", se[1]) + countLog(seats[2].h.log, "se", se[2]) > 0,
|
|
"member heard the bonus SE from sSoundEffects")
|
|
local mi = m2.sim.game.monInfo[1]
|
|
eq(mi.personality, 0x2000, "member 1's personality reached member 2 through the leader")
|
|
local scoreBefore = g0.comm.jumpScore
|
|
local rowBefore = g0.comm.jumpsInRow
|
|
seats[0].gone = true
|
|
hub:drop(0)
|
|
stepAll(seats, 5, function() return false end)
|
|
check(m1:isLeader(), "seat 0 dropped: seat 1 leads")
|
|
eq(m2.leader, 1, "seat 2 follows seat 1")
|
|
check(m1.sim.game:isLeader(), "the new leader's sim runs the leader funcs")
|
|
check(m1.sim.game.gone[0] and m2.sim.game.gone[0], "the dropped seat's mon is out of the game")
|
|
stepAll(seats, 20000, function()
|
|
return m1.sim.game.comm.jumpsInRow >= rowBefore + 3
|
|
end)
|
|
local g1 = m1.sim.game
|
|
check(g1.comm.jumpsInRow >= rowBefore + 3, "the swing goes on under the new leader (" .. g1.comm.jumpsInRow .. ")")
|
|
check(g1.comm.jumpScore > scoreBefore, "score carried over and keeps growing")
|
|
stepAll(seats, 10, function() return m2.sim.game.comm.jumpScore == g1.comm.jumpScore end)
|
|
eq(m2.sim.game.comm.jumpScore, g1.comm.jumpScore, "seat 2 follows the new leader's score")
|
|
|
|
print("[test] 6. a missed jump ends the round for everyone; records; NO drops the link")
|
|
seats[2].jump = false
|
|
seats[1].answer = "yes"
|
|
seats[2].answer = "no"
|
|
local sawPrompt1, sawPrompt2, sawShake = false, false, false
|
|
stepAll(seats, 20000, function()
|
|
local sim1, sim2 = m1.sim, m2.sim
|
|
if sim1 and sim1.game.gfx.yesno then sawPrompt1 = true end
|
|
if sim2 and sim2.game.gfx.yesno then sawPrompt2 = true end
|
|
if sim1 and sim1.game.gfx:isMonHitShakeActive(2) then sawShake = true end
|
|
return m1:finished() and m2:finished()
|
|
end)
|
|
check(sawShake, "the leader shook the hit mon (SpriteCB_MonHitShake)")
|
|
check(countLog(seats[1].h.log, "se", Game.SE_POKE_JUMP_FAILURE) > 0, "SE_POKE_JUMP_FAILURE")
|
|
check(sawPrompt1 and sawPrompt2, "both seats got \"Want to play again?\" with YES/NO")
|
|
eq(m1.phase, "done", "new leader done")
|
|
eq(m2.phase, "done", "member done")
|
|
check(m1.result ~= nil and m2.result ~= nil, "both seats got game3_mg_result")
|
|
eq(Json.encode(m1.result.results), Json.encode(m2.result.results), "results agree")
|
|
local final = g1.best
|
|
check(final.score > 0 and final.jumpsInRow > 0, "best score/jumps in the result")
|
|
eq(seats[1].session.pokemonJumpRecords.bestJumpScore, final.score, "seat 1 record written from the result")
|
|
eq(seats[2].session.pokemonJumpRecords.jumpsInRow, final.jumpsInRow, "seat 2 jumps-in-a-row record")
|
|
eq(countLog(seats[1].h.log, "records"), 1, "TryUpdateRecords once per game (AskPlayAgain)")
|
|
eq(countLog(seats[2].h.log, "records"), 1, "member too")
|
|
check(m2.sim.game.exited and m1.sim.game.exited, "both ran ClosePokeJumpLink to the fade")
|
|
eq(#hub.dropped, 0, "nothing the relay would drop")
|
|
check(hub.maxState <= 512 and hub.maxInput <= 512 and hub.maxResult <= 1024,
|
|
string.format("byte caps (state %d, input %d, result %d)", hub.maxState, hub.maxInput, hub.maxResult))
|
|
eq(countLog(seats[1].h.log, "maxgames"), 0, "three players: no five-player game counted")
|
|
end
|
|
|
|
print("[test] 7. prize, bag limits, save, play again")
|
|
do
|
|
local hub, seats = makeSeats(2)
|
|
seats[0].dismiss, seats[1].dismiss = true, true
|
|
local m0, m1 = seats[0].match, seats[1].match
|
|
stepAll(seats, 20000, function()
|
|
return m0.sim and m0.sim.game.comm.jumpsInRow >= 2
|
|
end)
|
|
local g0 = m0.sim.game
|
|
g0.comm.jumpScore = 7990
|
|
stepAll(seats, 20000, function() return g0.comm.jumpScore >= 8000 end)
|
|
seats[1].h.limit = 1
|
|
seats[0].jump, seats[1].jump = false, false
|
|
local prizeMsg0, prizeMsg1, saving = nil, nil, false
|
|
stepAll(seats, 20000, function()
|
|
local w0 = m0.sim.game.gfx.msgWindow
|
|
local w1 = m1.sim.game.gfx.msgWindow
|
|
if w0 and w0.shown and w0.key == "gText_AwesomeWonF701F700" then prizeMsg0 = w0 end
|
|
if w1 and w1.shown and w1.key == "gText_AwesomeWonF701F700" then prizeMsg1 = w1 end
|
|
if w0 and w0.key == "gText_SavingDontTurnOffPower" and w0.shown then saving = true end
|
|
return m0.sim.game.comm.funcId == F.ASK_PLAY_AGAIN
|
|
end)
|
|
check(prizeMsg0 ~= nil and prizeMsg1 ~= nil, "both seats print the prize message")
|
|
eq(prizeMsg0 and prizeMsg0.quantity, 2, "8000 points: two berries")
|
|
eq(prizeMsg1 and prizeMsg1.itemId, prizeMsg0 and prizeMsg0.itemId, "same berry for every seat (leader's comm.data)")
|
|
local item = prizeMsg0 and prizeMsg0.itemId
|
|
local isPrize = false
|
|
for _, id in ipairs(tables.prize_items) do if id == item then isPrize = true end end
|
|
check(isPrize, "prize is one of sPrizeItems (" .. tostring(item) .. ")")
|
|
eq(seats[0].h.bag[item], 2, "seat 0 bag got both berries")
|
|
eq(seats[1].h.bag[item], 1, "seat 1 bag limited by CheckBagHasSpace")
|
|
eq(countLog(seats[0].h.log, "fanfare", 257), 1, "MUS_LEVEL_UP fanfare")
|
|
check(saving, "SAVING... DON'T TURN OFF THE POWER.")
|
|
eq(countLog(seats[0].h.log, "save"), 1, "Task_LinkFullSave on the leader")
|
|
eq(countLog(seats[1].h.log, "save"), 1, "and on the member")
|
|
eq(countLog(seats[0].h.log, "records"), 1, "records written in SavePokeJump")
|
|
seats[0].answer, seats[1].answer = "yes", "yes"
|
|
local introAgain = false
|
|
stepAll(seats, 20000, function()
|
|
local g = m1.sim.game
|
|
if g.comm.funcId == F.GAME_INTRO then introAgain = true end
|
|
return m0.sim.game.gamesStarted >= 2 and m1.sim.game.gamesStarted >= 2
|
|
end)
|
|
check(introAgain, "YES + YES resets the game")
|
|
eq(m0.sim.game.comm.jumpScore, 0, "score back to 0")
|
|
check(m1.sim.game.gfx.countdown ~= nil, "the replay plays its own minigame countdown")
|
|
seats[0].answer, seats[1].answer = "b", "yes"
|
|
stepAll(seats, 20000, function() return m0:finished() and m1:finished() end)
|
|
eq(m0.phase, "done", "B at the prompt ends the link (leader)")
|
|
eq(m1.phase, "done", "member follows")
|
|
eq(countLog(seats[0].h.log, "records"), 3, "records once more for the second game")
|
|
eq(#hub.dropped, 0, "nothing dropped")
|
|
end
|
|
|
|
print("[test] 8. five players count toward gamesWithMaxPlayers; bag full message")
|
|
do
|
|
local hub, seats = makeSeats(5)
|
|
stepAll(seats, 3)
|
|
for s = 0, 4 do
|
|
eq(countLog(seats[s].h.log, "maxgames"), 1, "seat " .. s .. " IncrementGamesWithMaxPlayers")
|
|
end
|
|
local sim, h = standalone()
|
|
local g = sim.game
|
|
h.full = true
|
|
g.comm.data = 1 * 0x1000 + 138
|
|
g.helperState = 0
|
|
g.joy = {}
|
|
local texts = {}
|
|
for _ = 1, 400 do
|
|
g.joy = { a = (g.helperState == 5) }
|
|
local r = g:tryGivePrize()
|
|
g.gfx:update()
|
|
local w = g.gfx.msgWindow
|
|
if w and w.shown then texts[w.key] = true end
|
|
if not r then break end
|
|
end
|
|
check(texts.gText_AwesomeWonF701F700, "prize message first")
|
|
check(texts.gText_CantHoldMore, "full bag: You can't hold any more!")
|
|
eq(h.bag[138], nil, "nothing added")
|
|
end
|
|
|
|
print("[test] 9. snapshot carries what a new leader needs")
|
|
do
|
|
local sim = standalone({ n = 3 })
|
|
local g = sim.game
|
|
g.excellentsInRow, g.excellentsInRowRecord = 3, 4
|
|
g.numPlayersAtPeak, g.initScoreUpdate, g.giveBonus = 2, true, true
|
|
g.atJumpPeak3[0], g.atJumpPeak3[2] = true, true
|
|
g.best.score = 1230
|
|
local s = Wire.sanitize(Json.decode(Json.encode({ type = MG.MSG.STATE, f = 3, e = 1, s = sim:snapshot() }))).s
|
|
local member = standalone({ n = 3, seat = 1 })
|
|
member.game.comm.funcId = F.GAME_ROUND
|
|
member.game.funcActive = true
|
|
member:becomeLeader(s)
|
|
local mg = member.game
|
|
check(mg:isLeader(), "becomeLeader switches the funcs")
|
|
eq(mg.excellentsInRowRecord, 4, "excellents record carried")
|
|
eq(mg.numPlayersAtPeak, 2, "peak count carried")
|
|
check(mg.giveBonus and mg.atJumpPeak3[0] and mg.atJumpPeak3[2] and not mg.atJumpPeak3[1], "pending bonus carried")
|
|
eq(mg.best.score, 1230, "session best carried")
|
|
check(mg.gone[0], "old leader is out")
|
|
local big = standalone({ n = 5 }).game
|
|
for i = 0, 4 do
|
|
big.monInfo[i].personality, big.monInfo[i].species, big.monInfo[i].shiny = 0xFFFFFFFF, 412, true
|
|
local p = big.players[i]
|
|
p.monState, p.jumpState, p.jumpTimeStart, p.funcFinished = 2, 2, 65535, true
|
|
big.memberFuncIds[i], big.playAgainStates[i] = 9, 2
|
|
big.atJumpPeak2[i], big.atJumpPeak3[i] = true, true
|
|
end
|
|
big.comm.funcId, big.comm.data, big.comm.jumpsInRow, big.comm.jumpScore, big.comm.receivedBonusFlags = 9, 65535, 9999, 99990, 31
|
|
big.excellentsInRow, big.excellentsInRowRecord, big.numPlayersAtPeak = 9999, 9999, 5
|
|
big.best = { score = 99990, jumpsInRow = 9999, excellentsInRow = 9999 }
|
|
local bytes = #Json.encode({ type = MG.MSG.STATE, f = 2147483647, e = 2147483647, s = big:leaderPacketOut(true) })
|
|
check(bytes <= 512, "worst-case five-seat state with mon info fits the 512-byte cap (" .. bytes .. ")")
|
|
local ibytes = #Json.encode({ type = MG.MSG.INPUT, f = 2147483647, i = big:memberPacketOut() })
|
|
check(ibytes <= 512, "member input fits (" .. ibytes .. ")")
|
|
end
|
|
|
|
finish()
|