Files
gen1recomp/tests/game3_save_trainer_card_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

256 lines
9.6 KiB
Lua

-- Test game3 TrainerCard and SaveMenu mechanics and state flows.
local function test(name, fn)
local ok, err = pcall(fn)
if ok then
print("[ok] " .. name)
else
print("[FAIL] " .. name .. ": " .. tostring(err))
error(err)
end
end
print("[test] 1. Trainer Card lifecycle and fields")
local TrainerCard = require("src.ui.game3.trainer_card")
local session = {
name = "RED",
id = 12345,
money = 3500,
dex = { caught = { [1] = true, [4] = true, [7] = true } },
playTimeHours = 2,
playTimeMinutes = 45,
flags = { [0x820] = true, [0x821] = true },
-- sessions carry the engine's map id; the save screen resolves the place name
-- from it (see tests/engine/save_menu_location_bug2328.lua)
map = "FR_PALLET_TOWN",
}
local closed = false
TrainerCard.show({
session = session,
onClose = function() closed = true end,
})
test("TrainerCard is open", function()
assert(TrainerCard.isOpen() == true, "TrainerCard should be open")
end)
TrainerCard.close()
test("TrainerCard closed callback called", function()
assert(TrainerCard.isOpen() == false, "TrainerCard should be closed")
assert(closed == true, "onClose should have been called")
end)
test("TrainerCard front and back flip", function()
TrainerCard.show({ session = session })
assert(TrainerCard.isOpen() == true, "Should be open")
assert(TrainerCard.side == "front", "Initial side should be front")
local inpA = { wasPressed = function(_, k) return k == "a" end }
local inpB = { wasPressed = function(_, k) return k == "b" end }
local function settle()
for _ = 1, 64 do TrainerCard.update(1 / 60) end
end
-- src/trainer_card.c:558 A on the front starts the flip to the back
TrainerCard.handleInput(inpA)
settle()
assert(TrainerCard.side == "back", "Side should flip to back")
-- src/trainer_card.c:588 B on the back flips to the front
TrainerCard.handleInput(inpB)
settle()
assert(TrainerCard.side == "front", "Side should flip back to front")
-- src/trainer_card.c:566 B on the front closes the card
TrainerCard.handleInput(inpB)
assert(TrainerCard.isOpen() == false, "Should be closed after B")
end)
test("A on the back closes the card", function()
-- src/trainer_card.c:604 A on the back exits instead of flipping
TrainerCard.show({ session = session })
local inpA = { wasPressed = function(_, k) return k == "a" end }
TrainerCard.handleInput(inpA)
for _ = 1, 64 do TrainerCard.update(1 / 60) end
assert(TrainerCard.side == "back", "Should be on the back")
TrainerCard.handleInput(inpA)
assert(TrainerCard.isOpen() == false, "A on the back should close the card")
end)
print("[test] 2. SaveMenu lifecycle and state machine")
local SaveMenu = require("src.ui.game3.save_menu")
local Runtime = require("src.core.game3.runtime")
Runtime._game = { saveGame = function() return true end }
local saveClosed = false
SaveMenu.show({
session = session,
onClose = function() saveClosed = true end,
})
test("SaveMenu starts in confirm phase", function()
assert(SaveMenu.isOpen() == true, "SaveMenu should be open")
assert(SaveMenu._phase == "confirm", "Initial phase should be confirm")
assert(SaveMenu.cursor == 1, "Cursor should start on YES (1)")
end)
test("SaveMenu and TrainerCard agree on the badge flags", function()
assert(SaveMenu.countBadges(session) == 2, "save stats reads 2 badges from session.flags")
assert(TrainerCard.countBadges(session) == 2, "trainer card reads 2 badges from session.flags")
end)
test("SaveMenu cursor movement", function()
SaveMenu.move(1)
assert(SaveMenu.cursor == 2, "Cursor should flip to NO (2)")
SaveMenu.move(1)
assert(SaveMenu.cursor == 1, "Cursor should flip back to YES (1)")
end)
test("SaveMenu confirm transitions to overwrite then saved", function()
SaveMenu.confirm()
assert(SaveMenu._phase == "overwrite", "Selecting YES in confirm should transition to overwrite")
SaveMenu.confirm()
assert(SaveMenu._phase == "saved", "Selecting YES in overwrite should transition to saved")
SaveMenu.confirm()
assert(SaveMenu.isOpen() == false, "Confirm in saved phase should close menu")
assert(saveClosed == true, "Save onClose callback should be called")
end)
print("[test] 3. SaveMenu cancellation")
test("SaveMenu cancellation", function()
local noClosed = false
SaveMenu.show({
session = session,
onClose = function() noClosed = true end,
})
SaveMenu.move(1) -- Move to NO
SaveMenu.confirm() -- Press A on NO
assert(SaveMenu.isOpen() == false, "Selecting NO should close SaveMenu")
assert(noClosed == true, "onClose should be called on cancel")
end)
print("[test] 4. Trainer Card badge unlocking & multi-format resolution")
test("Badges from array format", function()
local s = { badges = { true, true, true, false, false, false, false, false } }
assert(TrainerCard.countBadges(s) == 3, "Should count 3 badges from array")
assert(TrainerCard.isBadgeUnlocked(1) == false, "Default closed should be false")
TrainerCard.show({ session = s })
assert(TrainerCard.isBadgeUnlocked(1) == true, "Badge 1 unlocked")
assert(TrainerCard.isBadgeUnlocked(2) == true, "Badge 2 unlocked")
assert(TrainerCard.isBadgeUnlocked(3) == true, "Badge 3 unlocked")
assert(TrainerCard.isBadgeUnlocked(4) == false, "Badge 4 locked")
TrainerCard.close()
end)
test("Badges from FRLG flags format (0x820..0x827)", function()
local s = {
flags = {
[0x820] = true, -- Boulder
[0x821] = true, -- Cascade
[0x823] = true, -- Rainbow
[0x826] = true, -- Volcano
}
}
assert(TrainerCard.countBadges(s) == 4, "Should count 4 badges from flags")
TrainerCard.show({ session = s })
assert(TrainerCard.isBadgeUnlocked(1) == true, "Boulder badge unlocked (0x820)")
assert(TrainerCard.isBadgeUnlocked(2) == true, "Cascade badge unlocked (0x821)")
assert(TrainerCard.isBadgeUnlocked(3) == false, "Thunder badge locked (0x822)")
assert(TrainerCard.isBadgeUnlocked(4) == true, "Rainbow badge unlocked (0x823)")
assert(TrainerCard.isBadgeUnlocked(7) == true, "Volcano badge unlocked (0x826)")
assert(TrainerCard.isBadgeUnlocked(8) == false, "Earth badge locked (0x827)")
TrainerCard.close()
end)
test("Badges from scripting store flags", function()
local s = {
store = {
flags = {
["FLAG_BADGE01_GET"] = true,
["FLAG_BADGE03_GET"] = true,
["FLAG_BADGE05_GET"] = true,
}
}
}
assert(TrainerCard.countBadges(s) == 3, "Should count 3 badges from store.flags")
TrainerCard.show({ session = s })
assert(TrainerCard.isBadgeUnlocked(1) == true, "Badge 1 (Boulder)")
assert(TrainerCard.isBadgeUnlocked(3) == true, "Badge 3 (Thunder)")
assert(TrainerCard.isBadgeUnlocked(5) == true, "Badge 5 (Soul)")
assert(TrainerCard.isBadgeUnlocked(2) == false, "Badge 2 (Cascade)")
TrainerCard.close()
end)
test("Badges from bitmask format (0xFF = all 8 badges)", function()
local s = { badges = 0xFF }
assert(TrainerCard.countBadges(s) == 8, "Should count 8 badges from 0xFF bitmask")
TrainerCard.show({ session = s })
for i = 1, 8 do
assert(TrainerCard.isBadgeUnlocked(i) == true, "Badge " .. i .. " unlocked")
end
TrainerCard.close()
end)
print("[test] 5. Currency glyph mapping and font measure")
test("Currency sign maps to Pokédollar 0xB7", function()
local FrlgFont = require("src.ui.game3.frlg_font")
assert(FrlgFont.glyphId("$") == 0xB7, "$ must map to Pokédollar glyph 0xB7")
assert(FrlgFont.glyphId("¥") == 0xB7, "¥ must map to Pokédollar glyph 0xB7")
assert(FrlgFont.glyphId("\xC2\xA5") == 0xB7, "UTF-8 yen must map to Pokédollar glyph 0xB7")
local w1 = FrlgFont.measure("$3000")
local w2 = FrlgFont.measure("¥3000")
assert(w1 == w2, "Width of $3000 and ¥3000 must match")
assert(w1 > 0, "Width must be positive")
end)
print("[test] 6. Runtime playtime accumulation and synchronization")
test("Runtime.pumpRtc increments and synchronizes playtime", function()
local Runtime = require("src.core.game3.runtime")
local sess = {
playtime = { hours = 1, minutes = 59, seconds = 58, vblanks = 0 }
}
local game = { session = sess, save = {} }
Runtime.session = sess
Runtime._playTimeAcc = 0
-- Tick 2 seconds (120 frames at 1/60s)
for _ = 1, 120 do
Runtime.pumpRtc(game, 1 / 60)
end
assert(sess.playTimeHours == 2, "Hours should roll over to 2 (was " .. tostring(sess.playTimeHours) .. ")")
assert(sess.playTimeMinutes == 0, "Minutes should roll over to 0 (was " .. tostring(sess.playTimeMinutes) .. ")")
assert(sess.playTimeSeconds == 0, "Seconds should roll over to 0 (was " .. tostring(sess.playTimeSeconds) .. ")")
assert(game.save.playTimeHours == 2, "Save hours should synchronize to 2")
assert(game.save.playTimeMinutes == 0, "Save minutes should synchronize to 0")
end)
if not require("tests.game3_cache").mount() then
print("[skip] test 7 reads ROM map section names: " .. tostring(require("tests.game3_cache").reason))
print("[test] all passed")
os.exit(0)
end
print("[test] 7. SaveMenu location header resolution from ROM mapsec")
test("SaveMenu location resolution does not print engine internal map IDs", function()
local MapSectionsExtract = require("src.import.gba.map_sections_extract")
local tests = {
{ map = "FR_ROUTE_22", want = "ROUTE 22" },
{ map = "FR_PALLET_TOWN", want = "PALLET TOWN" },
{ map = "FR_VIRIDIAN_CITY", want = "VIRIDIAN CITY" },
{ map = "FR_CELADON_CITY_DEPARTMENT_STORE_2F", want = "CELADON DEPT." },
{ map = "FR_POKEMON_TOWER_3F", want = "POKéMON TOWER" },
{ map = "FR_ROUTE_1", want = "ROUTE 1" },
}
for _, item in ipairs(tests) do
local place = MapSectionsExtract.getPlaceName(item.map)
assert(place == item.want, string.format("Expected %s for map %s, got %s", item.want, item.map, tostring(place)))
end
end)
print("[test] all passed")