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

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()