Files
gen1recomp/tests/game3_pokedex_test.lua
T
1jamie 52ad498b8b feat(game3): self-contained ROM-derived FireRed extraction & cache contract alignment
- Align CacheContract firered overrides with pure ROM extraction
- Fix door animation extraction tile sizes, palettes, and strides
- Update storage chrome, pokedex, summary, and battle chrome extractors
- Ensure 100% self-contained ROM extraction without external dependencies
2026-09-09 14:14:40 -05:00

298 lines
13 KiB
Lua

-- Comprehensive test suite for authentic FRLG Pokédex subsystem in Game 3.
-- Tests:
-- 1. Offline data extraction verification & manifest loading.
-- 2. 9 Habitat categories (Grassland, Forest, Waters-Edge, Sea, Cave, Mountain, Rough-Terrain, Urban, Rare).
-- 3. 6 Sorting orders (Numerical Kanto, Numerical National, Alphabetical, Type, Lightest, Smallest).
-- 4. Wild encounter area lookup & route marker coordinates (Kanto & Sevii Islands 1-7).
-- 5. 2-page description flavor text (FireRed & LeafGreen), height/weight metrics, and scaling offsets.
-- 6. Interactive UI state machine transitions across all 8 sub-screens.
local love = _G.love or require("tests.love_stub")
_G.love = love
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local Dex = require("src.core.game3.dex")
local Pokemon = require("src.core.game3.pokemon")
local PokedexData = require("src.core.game3.pokedex_data")
local PokedexChrome = require("src.ui.game3.pokedex_chrome")
local Pokedex = require("src.ui.game3.pokedex")
Pokemon.install(nil)
print("[test] 1. Pokédex data layer initialization & manifest validation")
assert(PokedexData.init() == true, "PokedexData initialization succeeds")
assert(PokedexChrome.install() == true, "PokedexChrome installation succeeds")
local manifest = PokedexChrome._manifest or { count = 386 }
assert(manifest.count == 386, "Pokedex manifest reports 386 species")
print("[ok] Data layer & chrome initialized")
print("[test] 2. 9 Habitat categories & 4-mon mini-page layouts")
local expectedCategories = {
"grassland", "forest", "waters_edge", "sea", "cave", "mountain", "rough_terrain", "urban", "rare"
}
for _, cat in ipairs(expectedCategories) do
local pages = PokedexData.getCategoryPages(cat)
assert(type(pages) == "table" and #pages > 0, "Category " .. cat .. " has pages (count=" .. #pages .. ")")
for pIdx, page in ipairs(pages) do
assert(#page >= 1 and #page <= 4, "Category " .. cat .. " page " .. pIdx .. " has 1-4 Pokémon")
for _, sp in ipairs(page) do
local nat = Pokemon.national and Pokemon.national(sp) or sp
assert(sp >= 1 and (sp <= 412 or (nat and nat <= 386)), "Valid species ID in category " .. cat)
end
end
end
-- Verify specific habitat memberships
local grasslandP1 = PokedexData.getCategoryPages("grassland")[1]
assert(grasslandP1[1] == 19, "Rattata is first mon on Grassland page 1")
assert(grasslandP1[2] == 20, "Raticate is second mon on Grassland page 1")
local cavePages = PokedexData.getCategoryPages("cave")
assert(#cavePages >= 5, "Cave category has at least 5 pages")
print("[ok] All 9 habitat categories and mini-pages verified")
print("[test] 3. 6 Sorting orders (Numerical Kanto/Nat, A-Z, Type, Weight, Height)")
local kantoList = PokedexData.getOrderList("numerical_kanto")
assert(#kantoList == 151, "Numerical Kanto has exactly 151 species")
assert(kantoList[1] == 1 and kantoList[151] == 151, "Kanto order runs 1 to 151")
local nationalList = PokedexData.getOrderList("numerical_national")
assert(#nationalList == 386, "Numerical National has 386 species")
assert(nationalList[1] == 1 and nationalList[386] == 386, "National order runs 1 to 386")
local atozList = PokedexData.getOrderList("atoz")
assert(#atozList == 386, "Alphabetical order has 386 species")
assert(atozList[1] == 63, "Abra (63) is first alphabetically")
assert(atozList[2] == 359, "Absol (359) is second alphabetically")
local lightestList = PokedexData.getOrderList("lightest")
assert(#lightestList == 386, "Weight order has 386 species")
assert(lightestList[1] == 92, "Gastly (92) is lightest (0.1 kg / 0.2 lbs)")
local smallestList = PokedexData.getOrderList("smallest")
assert(#smallestList == 386, "Height order has 386 species")
assert(smallestList[1] == 50, "Diglett (50) is smallest (0.2 m / 8 inches)")
local typeList = PokedexData.getOrderList("type")
assert(#typeList >= 386, "Type order has 386+ species")
print("[ok] All 6 sorting orders verified")
print("[test] 4. Species wild encounter area mapping & route coordinates")
local pikaAreas = PokedexData.getWildAreasForSpecies(25) -- Pikachu
assert(#pikaAreas == 2, "Pikachu is found in 2 wild areas in FireRed")
local hasViridian = false
local hasPowerPlant = false
for _, a in ipairs(pikaAreas) do
if a == "DEX_AREA_VIRIDIAN_FOREST" then hasViridian = true end
if a == "DEX_AREA_POWER_PLANT" then hasPowerPlant = true end
end
assert(hasViridian and hasPowerPlant, "Pikachu found in Viridian Forest and Power Plant")
local viridianMarker = PokedexData.getAreaMarker("DEX_AREA_VIRIDIAN_FOREST")
assert(viridianMarker ~= nil, "Viridian forest marker exists")
assert(viridianMarker.x == 51 and viridianMarker.y == 20, "Viridian forest marker at (51, 20)")
assert(viridianMarker.shape == "MARKER_CIRCULAR", "Viridian forest marker is circular")
local mewAreas = PokedexData.getWildAreasForSpecies(151) -- Mew (mythical, no wild route)
assert(#mewAreas == 0, "Mew has 0 wild encounter routes (Area Unknown)")
print("[ok] Wild encounter area mapping & marker definitions verified")
print("[test] 5. Pokédex 2-page flavor text & metric data")
local bulba = PokedexData.getEntry(1)
assert(bulba.category == "SEED", "Bulbasaur category is SEED")
assert(bulba.categoryName == "SEED POKéMON", "Bulbasaur category formatted as SEED POKéMON")
assert(bulba.heightDm == 7, "Bulbasaur height is 7 dm")
assert(bulba.weightHg == 69, "Bulbasaur weight is 69 hg")
assert(bulba.heightFormatted:find("2'04\"") ~= nil, "Bulbasaur height formatted 2'04\"")
assert(bulba.weightFormatted:find("15.2 lbs.") ~= nil, "Bulbasaur weight formatted 15.2 lbs.")
assert(bulba.pokemonScale == 356, "Bulbasaur pokemonScale is 356")
assert(bulba.pokemonOffset == 16, "Bulbasaur pokemonOffset is 16")
assert(bulba.trainerScale == 256, "Bulbasaur trainerScale is 256")
assert(bulba.trainerOffset == -2, "Bulbasaur trainerOffset is -2")
assert(bulba.description:find("plant seed on its back"), "FireRed page 1 flavor text loaded")
assert(bulba.description2:find("strange seed was planted") or bulba.description2:find("plant seed on its back"), "Pokédex page flavor text loaded")
print("[ok] 2-page flavor text and metric data verified")
print("[test] 6. Interactive Multi-Screen UI State Machine")
local mockInput = {
_pressed = {},
wasPressed = function(self, key) return self._pressed[key] == true end,
press = function(self, key) self._pressed = { [key] = true } end,
clear = function(self) self._pressed = {} end,
}
local testDex = Dex.new()
Dex.setSeen(testDex, 1) -- Bulbasaur (Grassland page 23)
Dex.setCaught(testDex, 1)
Dex.setSeen(testDex, 4) -- Charmander
Dex.setSeen(testDex, 19) -- Rattata (Grassland page 1 slot 1)
Dex.setSeen(testDex, 20) -- Raticate (Grassland page 1 slot 2)
Dex.setSeen(testDex, 25) -- Pikachu
Dex.setCaught(testDex, 25)
-- Open Top Menu (Mode Select)
Pokedex.show(testDex, { session = { dex = testDex } })
assert(Pokedex.isOpen() == true, "Pokédex is open")
assert(Pokedex.screen == "mode_select", "Pokédex opens on mode_select screen")
assert(Pokedex.modeCursor == 2, "Mode cursor starts on Numerical Mode (index 2)")
assert(Pokedex.MODES[Pokedex.modeCursor].id:find("numerical"), "Initial selection is Numerical Mode")
-- Navigate mode cursor down to Grassland (skips header index 3 to index 4)
mockInput:press("down")
Pokedex.handleInput(mockInput)
assert(Pokedex.modeCursor == 4, "Mode cursor moved to Grassland (index 4)")
assert(Pokedex.MODES[Pokedex.modeCursor].id == "grassland", "Grassland mode selected")
assert(Pokedex.MODES[Pokedex.modeCursor].unlocked == true, "Grassland is unlocked with Bulbasaur seen")
-- Test locked category behavior: navigate to an unseen category (e.g. sea)
-- Move down from grassland (4) -> forest (5) -> waters_edge (6) -> sea (7)
mockInput:press("down")
Pokedex.handleInput(mockInput) -- 5 (forest)
mockInput:press("down")
Pokedex.handleInput(mockInput) -- 6 (waters_edge)
mockInput:press("down")
Pokedex.handleInput(mockInput) -- 7 (sea)
assert(Pokedex.modeCursor == 7, "Mode cursor on Sea Pokémon (index 7)")
assert(Pokedex.MODES[Pokedex.modeCursor].unlocked == false, "Sea is locked with 0 sea mons seen")
-- Press A on locked category -> must NOT enter category_grid
mockInput:press("a")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "mode_select", "Cannot enter locked Sea category; stays on mode_select")
-- Move back up to Grassland (index 4)
mockInput:press("up")
Pokedex.handleInput(mockInput) -- 6
mockInput:press("up")
Pokedex.handleInput(mockInput) -- 5
mockInput:press("up")
Pokedex.handleInput(mockInput) -- 4 (grassland)
assert(Pokedex.modeCursor == 4, "Back on Grassland (index 4)")
-- Press A on unlocked Grassland -> opens Habitat Grid
mockInput:press("a")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "category_grid", "Enters category_grid screen for unlocked habitat")
assert(Pokedex.currentCategory == "grassland", "Current category is grassland")
assert(Pokedex.categoryPage == 1, "Starts on page 1")
-- Slot navigation within page 1
mockInput:press("down")
Pokedex.handleInput(mockInput)
assert(Pokedex.categorySlot == 2, "Moved slot down to 2")
mockInput:press("up")
Pokedex.handleInput(mockInput)
assert(Pokedex.categorySlot == 1, "Moved slot back up to 1")
-- Page flipping in Habitat Grid using R / L
mockInput:press("r")
Pokedex.handleInput(mockInput)
assert(Pokedex.categoryPage == 2, "Flipped to page 2 via R")
mockInput:press("l")
Pokedex.handleInput(mockInput)
assert(Pokedex.categoryPage == 1, "Flipped back to page 1 via L")
-- Press B in Habitat Grid -> returns to Mode Select
mockInput:press("b")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "mode_select", "Returns to mode_select on B")
-- Navigate to Numerical Kanto in Mode Select (index 2, skipping header index 3) and press A -> opens Ordered List
mockInput:press("up")
Pokedex.handleInput(mockInput)
assert(Pokedex.modeCursor == 2, "Mode cursor on Numerical Mode (index 2)")
assert(Pokedex.MODES[Pokedex.modeCursor].id:find("numerical"), "Selected Numerical Mode")
mockInput:press("a")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "ordered_list", "Enters ordered_list screen")
assert(Pokedex.listCursor == 1, "List cursor on No.001 Bulbasaur")
-- Press A on Bulbasaur -> opens Detailed Data Screen
mockInput:press("a")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "data", "Enters detailed data screen")
assert(Pokedex.selectedSpecies == 1, "Selected species is Bulbasaur")
assert(Pokedex.dataPage == 1, "Starts on description page 1 (FR)")
-- Press A to advance to Page 2 (Size Chart & Area Map)
mockInput:press("a")
Pokedex.handleInput(mockInput)
assert(Pokedex.dataPage == 2, "Advanced to Page 2 (Size Chart & Area Map)")
-- Press B on Page 2 -> returns to Page 1 (PREVIOUS DATA)
mockInput:press("b")
Pokedex.handleInput(mockInput)
assert(Pokedex.dataPage == 1, "Returned to Page 1 (Specs & Flavor text)")
-- Press B on Page 1 -> returns to Ordered List
mockInput:press("b")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "ordered_list", "Returns to ordered_list on B")
-- Navigate down to No.025 Pikachu and open Area Screen via action popup
for _ = 1, 24 do
mockInput:press("down")
Pokedex.handleInput(mockInput)
end
assert(Pokedex.listCursor == 25, "List cursor on Pikachu (25)")
-- Open Action Popup
Pokedex.actionCursor = 3 -- AREA
Pokedex.screen = "action_popup"
mockInput:press("a")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "area", "Enters area map screen for Pikachu")
-- Toggle Sevii Islands map on right arrow
mockInput:press("right")
Pokedex.handleInput(mockInput)
assert(Pokedex.areaMapKey == "one_island", "Toggled to One Island map")
mockInput:press("left")
Pokedex.handleInput(mockInput)
assert(Pokedex.areaMapKey == "kanto", "Toggled back to Kanto map")
-- Press B to return
mockInput:press("b")
Pokedex.handleInput(mockInput)
assert(Pokedex.screen == "ordered_list", "Returns to ordered_list from Area screen")
-- Verify draw passes for all Pokédex screens
print("[test] 7. Pokédex render passes for all screens")
Pokedex.open = true
local ok, err = pcall(function() Pokedex.draw() end)
if not ok then print("draw error:", err) end
assert(ok, "draw mode_select succeeds")
Pokedex.screen = "ordered_list"
assert(pcall(function() Pokedex.draw() end), "draw ordered_list succeeds")
Pokedex.screen = "data"
Pokedex.selectedSpecies = 1
assert(pcall(function() Pokedex.draw() end), "draw data screen succeeds")
Pokedex.screen = "area"
Pokedex.selectedSpecies = 25
assert(pcall(function() Pokedex.draw() end), "draw area screen succeeds")
Pokedex.screen = "category_grid"
Pokedex.currentCategory = "grassland"
assert(pcall(function() Pokedex.draw() end), "draw category_grid succeeds")
print("[ok] All Pokédex screen render passes verified")
-- Close Pokédex
mockInput:press("b")
Pokedex.handleInput(mockInput)
if Pokedex.screen == "mode_select" then
mockInput:press("b")
Pokedex.handleInput(mockInput)
end
Pokedex.close()
assert(Pokedex.isOpen() == false, "Pokédex is cleanly closed")
print("[ok] Multi-screen UI state machine transitions passed")
print("[ok] ALL GAME 3 POKÉDEX TESTS PASSED SUCCESSFULLY!")