mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
302 lines
13 KiB
Lua
302 lines
13 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
|
|
local Cache = require("tests.game3_cache")
|
|
if not Cache.mount("meta.json") then
|
|
print("[skip] online_mon3: " .. tostring(Cache.reason))
|
|
os.exit(0)
|
|
end
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
GameVersion.set("firered")
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local Party = require("src.core.game3.party")
|
|
local SummaryData = require("src.core.game3.summary_data")
|
|
local Protocol = require("src.link.Protocol")
|
|
local Json = require("src.link.Json")
|
|
Pokemon.install(nil)
|
|
T.check(Pokemon._names ~= nil, "FireRed species tables load from the cache")
|
|
|
|
local DATA = { pokemon = Pokemon }
|
|
local STRICT = { strict = true }
|
|
|
|
local function copy(t)
|
|
if type(t) ~= "table" then return t end
|
|
local out = {}
|
|
for k, v in pairs(t) do out[k] = copy(v) end
|
|
return out
|
|
end
|
|
|
|
local function wire(packed)
|
|
return Json.decode(Json.encode(packed))
|
|
end
|
|
|
|
local session = { party = {}, name = "RED", trainerId = 24680, secretId = 1357, gender = 0 }
|
|
local function give(species, level, extra)
|
|
local ok, _, mon = Party.giveMon(session, species, level)
|
|
assert(ok and mon, "giveMon " .. tostring(species))
|
|
for k, v in pairs(extra or {}) do mon[k] = v end
|
|
Pokemon.applyStats(mon)
|
|
session.party = {}
|
|
return mon
|
|
end
|
|
|
|
local function hostile(base, edit)
|
|
local p = copy(base)
|
|
edit(p)
|
|
return p
|
|
end
|
|
|
|
local function refused(packed, reason, label)
|
|
local mon, why = Protocol.unpackMon3(DATA, packed, STRICT)
|
|
T.eq(mon, nil, label .. " is refused")
|
|
T.eq(why, reason, label .. " names " .. reason)
|
|
end
|
|
|
|
do
|
|
local pika = give(25, 12, { item = 13, heldItem = 13, nickname = "SPARKY",
|
|
evs = { hp = 4, atk = 10, def = 0, spe = 252, spa = 0, spd = 0 } })
|
|
local packed = Protocol.packMon3(pika)
|
|
local keys = { "species", "nickname", "level", "exp", "hp", "status", "personality",
|
|
"otId", "otSecretId", "otName", "otGender", "nature", "ability", "gender", "ivs",
|
|
"evs", "moves", "item", "friendship", "pokerus", "metLocation", "metLevel",
|
|
"metGame", "pokeball", "ribbons", "markings", "isEgg", "fatefulEncounter",
|
|
"eggCycles" }
|
|
for _, key in ipairs(keys) do
|
|
T.check(packed[key] ~= nil, "packMon3 always emits " .. key)
|
|
end
|
|
T.eq(packed.status, "", "a healthy mon's status is the empty string")
|
|
T.eq(packed.item, 13, "the held item rides along")
|
|
T.eq(packed.otSecretId, 1357, "the secret id rides along")
|
|
|
|
local got, why = Protocol.unpackMon3(DATA, wire(packed), STRICT)
|
|
T.check(got ~= nil, "a legit party mon survives a strict round trip: " .. tostring(why))
|
|
T.eq(got.species, 25, "species")
|
|
T.eq(got.level, 12, "level")
|
|
T.eq(got.exp, pika.exp, "exp")
|
|
T.eq(got.personality, pika.personality, "personality")
|
|
T.eq(got.nickname, "SPARKY", "nickname")
|
|
T.eq(got.otName, "RED", "OT name")
|
|
T.eq(got.ot, "RED", "OT alias")
|
|
T.eq(got.otId, 24680, "OT id")
|
|
T.eq(got.nature, pika.nature, "nature")
|
|
T.eq(got.gender, pika.gender, "gender")
|
|
T.eq(got.ability, pika.ability, "ability")
|
|
T.eq(got.abilityId, pika.ability, "ability id alias")
|
|
T.same(got.ivs, pika.ivs, "IVs")
|
|
T.same(got.evs, pika.evs, "EVs")
|
|
T.same(got.moves, pika.moves, "move ids")
|
|
T.same(got.pp, pika.pp, "PP")
|
|
T.same(got.maxPp, pika.maxPp, "max PP")
|
|
T.eq(got.item, 13, "item")
|
|
T.eq(got.heldItem, 13, "heldItem alias")
|
|
T.eq(got.maxHp, pika.maxHp, "max HP recomputed identically")
|
|
T.eq(got.attack, pika.attack, "attack recomputed identically")
|
|
T.eq(got.speed, pika.speed, "speed recomputed identically")
|
|
T.eq(got.speciesNumbering, "internal", "internal numbering")
|
|
T.eq(got.name, Pokemon.name(25), "name from the ROM")
|
|
T.eq(got.status, nil, "empty status reads back as nil")
|
|
T.eq(got.isEgg, false, "not an egg")
|
|
T.check(got.stats and got.stats.hp == got.maxHp, "save_mon.normalize filled the stats block")
|
|
|
|
local tampered = copy(packed)
|
|
tampered.attack, tampered.maxHp = 999, 999
|
|
local t2 = Protocol.unpackMon3(DATA, tampered, STRICT)
|
|
T.eq(t2 and t2.attack, pika.attack, "wire stat fields are never read")
|
|
|
|
local hurt = copy(packed)
|
|
hurt.hp = 9999
|
|
local h = Protocol.unpackMon3(DATA, hurt, STRICT)
|
|
T.eq(h and h.hp, pika.maxHp, "hp is clamped to the computed max")
|
|
|
|
local forced = Protocol.unpackMon3(DATA, packed, { strict = true, forceLevel = 50 })
|
|
T.eq(forced and forced.level, 50, "forceLevel sets the level")
|
|
T.eq(forced and forced.exp, SummaryData.expForLevel(Pokemon.growthRate(25), 50),
|
|
"and the exp to that level's minimum")
|
|
T.eq(forced and forced.hp, forced and forced.maxHp, "a forced level starts at full HP")
|
|
end
|
|
|
|
do
|
|
local mon = give(64, 30)
|
|
mon.moves = { [1] = 93, [3] = 60 }
|
|
mon.pp = { [1] = 20, [3] = 22 }
|
|
mon.maxPp = { [1] = 25, [3] = 22 }
|
|
mon.item, mon.heldItem = nil, nil
|
|
local packed = Protocol.packMon3(mon)
|
|
T.eq(#packed.moves, 2, "a move list with a hole packs dense")
|
|
T.eq(packed.moves[1].id, 93, "first move")
|
|
T.eq(packed.moves[2].id, 60, "second move closes the gap")
|
|
T.eq(packed.moves[1].ppUps, 0, "PP ups inferred from max PP")
|
|
T.eq(packed.item, 0, "no held item packs as 0")
|
|
local encoded = Json.encode(packed)
|
|
T.check(encoded:find('"item":0', 1, true) ~= nil, "and item survives JSON")
|
|
local back = Json.decode(encoded)
|
|
T.eq(#back.moves, 2, "no hole truncates the JSON array")
|
|
|
|
local up = give(64, 30)
|
|
up.moves = { 93 }
|
|
local base = Pokemon.movePp(93)
|
|
up.ppBonusesPacked = 3
|
|
up.pp = { base + math.floor(base * 20 * 3 / 100) }
|
|
up.maxPp = { base + math.floor(base * 20 * 3 / 100) }
|
|
local upPacked = Protocol.packMon3(up)
|
|
T.eq(upPacked.moves[1].ppUps, 3, "packed PP bonuses ride as ppUps")
|
|
local upGot = Protocol.unpackMon3(DATA, wire(upPacked), STRICT)
|
|
T.eq(upGot and upGot.maxPp[1], up.maxPp[1], "PP Max survives the trip")
|
|
T.eq(upGot and upGot.ppBonusesPacked, 3, "into the engine's packed bonuses")
|
|
end
|
|
|
|
do
|
|
local egg
|
|
local ok, _, got = Party.giveEgg(session, 175)
|
|
egg = got
|
|
session.party = {}
|
|
T.check(ok and egg ~= nil, "a Togepi egg is made")
|
|
egg.eggCycles = 10
|
|
local packed = Protocol.packMon3(egg)
|
|
T.eq(packed.isEgg, true, "the egg flag rides along")
|
|
T.eq(packed.eggCycles, 10, "with its cycles")
|
|
local back, why = Protocol.unpackMon3(DATA, wire(packed), STRICT)
|
|
T.check(back ~= nil, "a legit egg survives a strict trip: " .. tostring(why))
|
|
T.eq(back and back.isEgg, true, "still an egg")
|
|
|
|
refused(hostile(packed, function(p) p.moves[1] = { id = 354, pp = 5, ppUps = 0 } end),
|
|
"bad egg", "an egg knowing PSYCHO BOOST")
|
|
local mewtwo = hostile(packed, function(p)
|
|
p.species = 150
|
|
p.gender = "U"
|
|
p.ability = Pokemon.abilities(150)[1]
|
|
p.moves = { { id = 93, pp = 25, ppUps = 0 } }
|
|
p.exp = SummaryData.expForLevel(Pokemon.growthRate(150), p.level)
|
|
end)
|
|
refused(mewtwo, "bad egg", "a Mewtwo egg")
|
|
local pika = give(25, 5)
|
|
local pikaEgg = Protocol.packMon3(pika)
|
|
pikaEgg.isEgg, pikaEgg.eggCycles = true, 10
|
|
pikaEgg.level, pikaEgg.exp = 5, SummaryData.expForLevel(Pokemon.growthRate(25), 5)
|
|
refused(pikaEgg, "bad egg", "a Pikachu egg, which only Pichu hatches from")
|
|
end
|
|
|
|
do
|
|
local Breeding = require("src.core.game3.breeding")
|
|
local noIncense = { mons = {} }
|
|
for _, pair in ipairs({ { 350, 183, "MARILL" }, { 360, 202, "WOBBUFFET" } }) do
|
|
local species = Breeding.alterEggSpeciesWithIncenseItem(pair[1], noIncense)
|
|
T.eq(species, pair[2], "without incense the cart breeds a " .. pair[3] .. " egg")
|
|
local ok, _, egg = Party.giveEgg(session, species)
|
|
session.party = {}
|
|
T.check(ok and egg ~= nil, "a " .. pair[3] .. " egg is made")
|
|
local back, why = Protocol.unpackMon3(DATA, wire(Protocol.packMon3(egg)), STRICT)
|
|
T.check(back ~= nil, "a bred " .. pair[3] .. " egg survives a strict trip: " .. tostring(why))
|
|
end
|
|
local marill = Protocol.packMon3(give(183, 5))
|
|
local marillEgg = hostile(marill, function(p) p.isEgg, p.eggCycles = true, 10 end)
|
|
T.check(Protocol.unpackMon3(DATA, wire(marillEgg), STRICT) ~= nil,
|
|
"a MARILL egg passes the species rule")
|
|
local pikaEgg = hostile(Protocol.packMon3(give(25, 5)), function(p)
|
|
p.isEgg, p.eggCycles = true, 10
|
|
end)
|
|
refused(pikaEgg, "bad egg", "while other evolved-species eggs stay refused")
|
|
end
|
|
|
|
do
|
|
local base = Protocol.packMon3(give(25, 20))
|
|
refused("PIKACHU", "bad mon", "a string")
|
|
refused(hostile(base, function(p) p.species = 0 end), "unknown species", "species 0")
|
|
refused(hostile(base, function(p) p.species = 260 end), "unknown species", "an old Unown slot")
|
|
refused(hostile(base, function(p) p.species = 412 end), "unknown species", "the egg species id")
|
|
refused(hostile(base, function(p) p.species = 9999 end), "unknown species", "species 9999")
|
|
refused(hostile(base, function(p) p.species = "PIKACHU" end), "unknown species", "a named species")
|
|
refused(hostile(base, function(p) p.level = 0 end), "bad level", "level 0")
|
|
refused(hostile(base, function(p) p.level = 101 end), "bad level", "level 101")
|
|
refused(hostile(base, function(p) p.level = 100 end), "bad level", "level 100 on level 20 exp")
|
|
refused(hostile(base, function(p) p.moves[1].id = 0 end), "bad move", "move 0")
|
|
refused(hostile(base, function(p) p.moves[1].id = 355 end), "bad move", "move 355")
|
|
refused(hostile(base, function(p) p.moves = {} end), "bad move", "no moves")
|
|
refused(hostile(base, function(p)
|
|
p.moves = { p.moves[1], copy(p.moves[1]) }
|
|
end), "bad move", "a duplicate move")
|
|
refused(hostile(base, function(p) p.moves[1].pp = 99 end), "bad move", "PP over the max")
|
|
refused(hostile(base, function(p) p.moves[1].ppUps = 4 end), "bad move", "four PP ups")
|
|
refused(hostile(base, function(p)
|
|
for i = 1, 5 do p.moves[i] = { id = i, pp = 1, ppUps = 0 } end
|
|
end), "bad move", "five moves")
|
|
refused(hostile(base, function(p) p.item = 375 end), "bad item", "item 375")
|
|
refused(hostile(base, function(p) p.item = -1 end), "bad item", "item -1")
|
|
refused(hostile(base, function(p) p.ivs.atk = 32 end), "bad ivs", "an IV of 32")
|
|
refused(hostile(base, function(p) p.evs.spe = 256 end), "bad evs", "an EV of 256")
|
|
refused(hostile(base, function(p)
|
|
p.evs = { hp = 255, atk = 255, def = 1, spe = 0, spa = 0, spd = 0 }
|
|
end), "bad evs", "511 total EVs")
|
|
refused(hostile(base, function(p) p.nature = (p.nature + 1) % 25 end),
|
|
"nature mismatch", "a nature the personality can't give")
|
|
refused(hostile(base, function(p) p.gender = p.gender == "M" and "F" or "M" end),
|
|
"gender mismatch", "a flipped gender")
|
|
refused(hostile(base, function(p) p.ability = 1 end), "bad ability", "STENCH on Pikachu")
|
|
refused(hostile(base, function(p) p.otName = "ABCDEFGH" end), "bad ot", "an 8-letter OT")
|
|
refused(hostile(base, function(p) p.otId = 70000 end), "bad ot", "an OT id past 65535")
|
|
refused(hostile(base, function(p) p.otGender = 2 end), "bad ot", "OT gender 2")
|
|
refused(hostile(base, function(p) p.nickname = "ABCDEFGHIJK" end), "bad nickname",
|
|
"an 11-letter nickname")
|
|
refused(hostile(base, function(p) p.nickname = "A\nB" end), "bad nickname",
|
|
"a nickname with a control character")
|
|
|
|
local accent = Protocol.unpackMon3(DATA, hostile(base, function(p) p.nickname = "POKéMONéé" end),
|
|
STRICT)
|
|
T.eq(accent and accent.nickname, "POKéMONéé", "names count characters, not bytes")
|
|
|
|
local loose = Protocol.unpackMon3(DATA, hostile(base, function(p)
|
|
p.ivs.atk, p.level, p.item = 40, 150, 999
|
|
end), { strict = false })
|
|
T.eq(loose and loose.ivs.atk, 31, "non-strict clamps an IV")
|
|
T.eq(loose and loose.level, 100, "and a level")
|
|
T.eq(loose and loose.item, 374, "and an item")
|
|
local unknown, why = Protocol.unpackMon3(DATA, hostile(base, function(p) p.species = 9999 end),
|
|
{ strict = false })
|
|
T.eq(unknown, nil, "non-strict still refuses an unknown species")
|
|
T.eq(why, "unknown species", "by name")
|
|
end
|
|
|
|
do
|
|
local failures, total = {}, 0
|
|
for species = 1, 411 do
|
|
if Pokemon.isInternalSpecies(species) then
|
|
for _, level in ipairs({ 1, 5, 37, 100 }) do
|
|
total = total + 1
|
|
local packed = Protocol.packMon3(give(species, level))
|
|
local got, why = Protocol.unpackMon3(DATA, wire(packed), STRICT)
|
|
if not got then failures[#failures + 1] = ("%d@%d:%s"):format(species, level, why) end
|
|
end
|
|
end
|
|
end
|
|
T.check(total > 1500, "the sweep covers every species")
|
|
T.eq(#failures, 0, "every species at every level round-trips strictly: "
|
|
.. table.concat(failures, " ", 1, math.min(#failures, 8)))
|
|
end
|
|
|
|
do
|
|
T.eq(Protocol.canonical({ b = 1, a = { 2, 3 }, c = "x\"y" }), '{"a":[2,3],"b":1,"c":"x\\"y"}',
|
|
"canonical JSON sorts keys and escapes like Json")
|
|
T.eq(Protocol.canonical({ n = 2.9, t = true, f = false, e = {} }),
|
|
'{"e":[],"f":false,"n":2,"t":true}', "integers only, booleans, empty tables as arrays")
|
|
local a = Protocol.packMon3(give(25, 20))
|
|
local b = Protocol.packMon3(give(64, 30))
|
|
local wa, wb = Protocol.wireMon3(a), Protocol.wireMon3(b)
|
|
local d = Protocol.tradeDigest(wa, wb)
|
|
T.check(type(d) == "string" and d:match("^[0-9a-f]+$") and #d == 16, "the digest is 16 hex")
|
|
T.eq(Protocol.tradeDigest(wa, wb), d, "the digest is stable")
|
|
T.check(Protocol.tradeDigest(wb, wa) ~= d, "seat order matters")
|
|
T.eq(Protocol.tradeDigest(wire(wa), wire(wb)), d, "a JSON trip does not move it")
|
|
T.eq(Protocol.tradeDigest(Protocol.wireMon3(wire(wa)), Protocol.wireMon3(wire(wb))), d,
|
|
"nor does sanitizing it again on the far side")
|
|
local other = copy(wa)
|
|
other.personality = (other.personality + 1) % 4294967296
|
|
T.check(Protocol.tradeDigest(other, wb) ~= d, "any field change moves the digest")
|
|
T.eq(#Protocol.packParty3({ give(25, 5), give(1, 5), give(4, 5) }, { 3, 1 }), 2,
|
|
"packParty3 packs the chosen slots")
|
|
end
|
|
|
|
T.finish()
|