-- luajit tests/online_convert.lua package.path = "./?.lua;./?/init.lua;" .. package.path love = love or require("tests.love_stub") local T = require("tests.harness") local Convert = require("src.online.Convert") -------------------------------------------------------------------------- -- Synthetic datasets -------------------------------------------------------------------------- local gen1Data = { pokemon = { TESTMON = { id = "TESTMON", name = "TESTMON", index = 1, dex = 1, types = { "GRASS", "POISON" }, baseStats = { hp = 45, attack = 49, defense = 49, speed = 45, special = 65 }, catchRate = 45, growthRate = "MEDIUM_SLOW", }, MAGNETIC = { id = "MAGNETIC", name = "MAGNETIC", index = 2, dex = 2, types = { "ELECTRIC" }, baseStats = { hp = 25, attack = 35, defense = 70, speed = 45, special = 95 }, catchRate = 190, growthRate = "MEDIUM_FAST", }, }, moves = { TACKLE = { id = "TACKLE", name = "TACKLE", pp = 35 }, VINE_WHIP = { id = "VINE_WHIP", name = "VINE WHIP", pp = 10 }, }, } local gen2Data = { pokemon = { TESTMON = { id = "TESTMON", name = "TESTMON", types = { "GRASS", "POISON" }, baseStats = { hp = 45, attack = 49, defense = 49, speed = 45, specialAttack = 65, specialDefense = 65 }, growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 0x1f, }, SPLITMON = { id = "SPLITMON", name = "SPLITMON", types = { "WATER" }, baseStats = { hp = 60, attack = 60, defense = 60, speed = 60, specialAttack = 90, specialDefense = 40 }, growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 0x7f, }, MAGNETIC = { id = "MAGNETIC", name = "MAGNETIC", types = { "ELECTRIC", "STEEL" }, baseStats = { hp = 25, attack = 35, defense = 70, speed = 45, specialAttack = 95, specialDefense = 55 }, growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 0xff, }, JOHTOMON = { id = "JOHTOMON", name = "JOHTOMON", types = { "FIRE" }, baseStats = { hp = 50, attack = 65, defense = 64, speed = 45, specialAttack = 60, specialDefense = 50 }, growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 0x1f, }, growthRates = { GROWTH_MEDIUM_SLOW = { numerator = 6, denominator = 5, squared = -15, linear = 100, constant = 140 }, GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1 }, }, }, moves = { TACKLE = { id = "TACKLE", name = "TACKLE", pp = 35 }, VINE_WHIP = { id = "VINE_WHIP", name = "VINE WHIP", pp = 10 }, SHADOW_BALL = { id = "SHADOW_BALL", name = "SHADOW BALL", pp = 15 }, }, items = { LEFTOVERS = { id = "LEFTOVERS", name = "LEFTOVERS" }, METAL_COAT = { id = "METAL_COAT", name = "METAL COAT" }, FLOWER_MAIL = { id = "FLOWER_MAIL", name = "FLOWER MAIL" }, }, } local DVS = { attack = 15, defense = 14, speed = 13, special = 12 } local ZERO_EXP = { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 } local Stats = require("src.pokemon.Stats") local Growth = require("src.pokemon.Growth") local Mon = require("src.battle.gen2.Mon") local function gen1Mon(species, level, over) species = species or "TESTMON" level = level or 30 local def = gen1Data.pokemon[species] local dvs = {} for k, v in pairs(DVS) do dvs[k] = v end dvs.hp = Mon.hpDV(dvs) local stats = Stats.calc(def, level, dvs, ZERO_EXP) local mon = { species = species, level = level, exp = Growth.expForLevel(def.growthRate, level), dvs = dvs, statExp = { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 }, stats = stats, hp = stats.hp, catchRate = def.catchRate, status = nil, moves = { { id = "TACKLE", pp = 35, ppUps = 0 }, { id = "VINE_WHIP", pp = 10, ppUps = 0 } }, nickname = "SPROUT", ot = "RED", otId = 1234, traded = false, } for k, v in pairs(over or {}) do mon[k] = v end return mon end local function gen2Mon(species, level, over) species = species or "TESTMON" level = level or 30 local def = gen2Data.pokemon[species] local dvs = {} for k, v in pairs(DVS) do dvs[k] = v end dvs.hp = Mon.hpDV(dvs) local statExp = { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 } local stats = Mon.stats(def.baseStats, dvs, level, statExp) local mon = { species = species, level = level, experience = Mon.experienceForLevel( Mon.growthFor(gen2Data, def.growthRate), level), dvs = dvs, statExp = statExp, stats = stats, hp = stats.hp, maxHp = stats.hp, item = nil, status = nil, happiness = 128, pokerus = 0, caughtLevel = 5, moves = { { id = "TACKLE", pp = 35, maxPp = 35 }, { id = "VINE_WHIP", pp = 10, maxPp = 10 } }, nickname = "SPROUT", ot = "GOLD", otName = "GOLD", otId = 4321, traded = false, isEgg = false, } for k, v in pairs(over or {}) do mon[k] = v end return mon end local function has(lines, needle) for _, line in ipairs(lines) do if line:find(needle, 1, true) then return true end end return false end local function kinds(list) local out = {} for _, row in ipairs(list) do out[row.kind] = row end return out end -------------------------------------------------------------------------- -- Gen 1 -> Gen 2 -------------------------------------------------------------------------- do local src = gen1Mon("TESTMON", 30) local out, report = Convert.toGen2(src, gen1Data, gen2Data) T.check(out ~= nil, "1->2 converts a Kanto mon") T.eq(out.species, "TESTMON", "1->2 keeps the species") T.eq(out.level, 30, "1->2 keeps the level") T.eq(out.experience, Mon.experienceForLevel(Mon.growthFor(gen2Data, "GROWTH_MEDIUM_SLOW"), 30), "1->2 recomputes experience on the Gen 2 curve") T.eq(out.dvs.hp, Mon.hpDV(src.dvs), "1->2 derives the HP DV") T.eq(out.happiness, 70, "1->2 stamps happiness 70") T.eq(out.pokerus, 0, "1->2 zeroes pokerus") T.eq(out.item, nil, "1->2 carries no held item") T.eq(out.caughtLevel, 30, "1->2 meets at the current level") T.eq(out.isEgg, false, "1->2 is never an egg") T.eq(out.nickname, "SPROUT", "1->2 keeps the nickname") T.eq(out.ot, "RED", "1->2 keeps the OT") T.eq(out.otName, "RED", "1->2 mirrors otName") T.eq(out.otId, 1234, "1->2 keeps the OT id") T.eq(out.maxHp, out.stats.hp, "1->2 sets maxHp from the new stats") T.eq(out.hp, out.stats.hp, "1->2 keeps a full-HP mon full") T.eq(out.stats.specialAttack, Mon.stats(gen2Data.pokemon.TESTMON.baseStats, out.dvs, 30, out.statExp).specialAttack, "1->2 recomputes stats through Mon.stats") T.eq(out.stats.specialAttack, out.stats.specialDefense, "1->2 splits one Special into two equal stats when the bases agree") T.eq(out.moves[1].maxPp, 35, "1->2 gives moves a maxPp from move data") T.eq(#report.lost, 0, "1->2 loses nothing") T.check(kinds(report.changed).happiness ~= nil, "1->2 reports the friendship") end do local src = gen1Mon("TESTMON", 50) src.species = "SPLITMON" src.stats = Stats.calc( { baseStats = { hp = 60, attack = 60, defense = 60, speed = 60, special = 65 } }, 50, src.dvs, ZERO_EXP) src.hp = src.stats.hp local out, report = Convert.toGen2(src, gen1Data, gen2Data) T.check(out.stats.specialAttack ~= out.stats.specialDefense, "1->2 splits Special apart when the Gen 2 bases differ") local row = kinds(report.changed).special_split T.check(row ~= nil, "1->2 reports the Special split") T.eq(row.text, ("SPECIAL SPLIT %d -> %d/%d"):format(src.stats.special, out.stats.specialAttack, out.stats.specialDefense), "1->2 spells the Special split in the game's voice") end do local src = gen1Mon("TESTMON", 30, { hp = 1 }) src.status = "PSN" local out = Convert.toGen2(src, gen1Data, gen2Data) T.eq(out.status, "poison", "1->2 maps PSN to poison") T.check(out.hp >= 1 and out.hp <= out.maxHp, "1->2 keeps a hurt mon alive after scaling") end do for from, to in pairs({ SLP = "sleep", PSN = "poison", BRN = "burn", PAR = "paralyze", FRZ = "freeze" }) do local out = Convert.toGen2(gen1Mon("TESTMON", 20, { status = from }), gen1Data, gen2Data) T.eq(out.status, to, "1->2 status " .. from) end end do local src = gen1Mon("TESTMON", 60) src.hp = math.floor(src.stats.hp / 2) local out = Convert.toGen2(src, gen1Data, gen2Data) local want = math.floor(src.hp * out.maxHp / src.stats.hp + 0.5) T.eq(out.hp, want, "1->2 scales HP proportionally to the new maximum") local fainted = Convert.toGen2(gen1Mon("TESTMON", 60, { hp = 0 }), gen1Data, gen2Data) T.eq(fainted.hp, 0, "1->2 keeps a fainted mon fainted") end do local src = gen1Mon("TESTMON", 30) src.moves = { { id = "TACKLE", pp = 42, ppUps = 1 } } local out = Convert.toGen2(src, gen1Data, gen2Data) T.eq(out.moves[1].maxPp, 35 + 7, "1->2 honours PP Ups in maxPp") T.eq(out.moves[1].pp, 42, "1->2 keeps current PP under the new cap") end do local shinyDvs = { attack = 15, defense = 10, speed = 10, special = 10 } local src = gen1Mon("TESTMON", 30, { dvs = shinyDvs }) local out = Convert.toGen2(src, gen1Data, gen2Data) T.check(out.shiny == true, "1->2 derives shininess from the DVs") T.eq(out.gender, "male", "1->2 derives gender from the Attack DV") local plain = Convert.toGen2(gen1Mon("TESTMON", 30, { dvs = { attack = 0, defense = 3, speed = 5, special = 7 } }), gen1Data, gen2Data) T.check(plain.shiny == false, "1->2 leaves an ordinary mon unshiny") T.eq(plain.gender, "female", "1->2 reads a low Attack DV as female") end -------------------------------------------------------------------------- -- Gen 2 -> Gen 1 -------------------------------------------------------------------------- do local src = gen2Mon("TESTMON", 30) local out, report = Convert.toGen1(src, gen2Data, gen1Data) T.check(out ~= nil, "2->1 converts a Kanto mon") T.eq(out.level, 30, "2->1 keeps the level") T.eq(out.exp, Growth.expForLevel("MEDIUM_SLOW", 30), "2->1 recomputes exp on the Gen 1 curve") T.eq(out.catchRate, 45, "2->1 stamps the Gen 1 species' own catch rate") T.eq(out.stats.special, Stats.calc(gen1Data.pokemon.TESTMON, 30, out.dvs, out.statExp).special, "2->1 folds Special back through the Gen 1 base Special") T.eq(out.stats.specialAttack, nil, "2->1 has no specialAttack") T.eq(out.nickname, "SPROUT", "2->1 keeps the nickname") T.eq(out.ot, "GOLD", "2->1 keeps the OT") T.eq(out.otId, 4321, "2->1 keeps the OT id") T.eq(out.moves[1].ppUps, 0, "2->1 derives PP Ups from maxPp") local lost = kinds(report.lost) T.check(lost.happiness ~= nil, "2->1 reports friendship lost") T.check(lost.caught_level ~= nil, "2->1 reports the met data lost") T.check(lost.item == nil, "2->1 reports no item when none is held") end do local src = gen2Mon("TESTMON", 40, { item = "LEFTOVERS", pokerus = 0xf1 }) local out, report = Convert.toGen1(src, gen2Data, gen1Data) T.check(out ~= nil, "2->1 accepts a mon holding an ordinary item") T.eq(out.item, nil, "2->1 drops the held item") T.eq(out.catchRate, 45, "2->1 does not put the item byte in the catch rate slot") local lost = kinds(report.lost) T.eq(lost.item.text, "HELD ITEM LOST: LEFTOVERS", "2->1 names the item it dropped") T.check(lost.pokerus ~= nil, "2->1 reports pokerus lost") end do for from, to in pairs({ sleep = "SLP", poison = "PSN", toxic = "PSN", burn = "BRN", paralyze = "PAR", freeze = "FRZ" }) do local out = Convert.toGen1(gen2Mon("TESTMON", 20, { status = from }), gen2Data, gen1Data) T.eq(out.status, to, "2->1 status " .. from) end end do local src = gen2Mon("SPLITMON", 50) local out, report = Convert.toGen1(src, gen2Data, gen1Data) T.check(out == nil, "2->1 refuses a species the Gen 1 dataset does not know") T.eq(report, "species_too_new", "2->1 gives the species refusal reason") end do local src = gen2Mon("JOHTOMON", 20) local out, reason, info = Convert.toGen1(src, gen2Data, gen1Data) T.check(out == nil, "2->1 refuses a Johto species") T.eq(reason, "species_too_new", "2->1 names the Johto refusal") T.eq(info.species, "JOHTOMON", "2->1 says which species was refused") end do local src = gen2Mon("TESTMON", 30) src.moves = { { id = "TACKLE", pp = 35, maxPp = 35 }, { id = "SHADOW_BALL", pp = 15, maxPp = 15 } } local out, reason, info = Convert.toGen1(src, gen2Data, gen1Data) T.check(out == nil, "2->1 refuses a move past STRUGGLE") T.eq(reason, "move_too_new", "2->1 names the move refusal") T.eq(info.move, "SHADOW_BALL", "2->1 says which move was refused") end do local src = gen2Mon("TESTMON", 30, { item = "FLOWER_MAIL" }) local out, reason = Convert.toGen1(src, gen2Data, gen1Data) T.check(out == nil, "2->1 refuses a mon holding mail") T.eq(reason, "has_mail", "2->1 names the mail refusal") local carried = gen2Mon("TESTMON", 30, { mail = { message = "HI", author = "GOLD" } }) local out2, reason2 = Convert.toGen1(carried, gen2Data, gen1Data) T.check(out2 == nil, "2->1 refuses a mon carrying a mail record") T.eq(reason2, "has_mail", "2->1 names the mail-record refusal") end do local src = gen2Mon("TESTMON", 30, { isEgg = true }) local out, reason = Convert.toGen1(src, gen2Data, gen1Data) T.check(out == nil, "2->1 refuses an egg") T.eq(reason, "is_egg", "2->1 names the egg refusal") end do local src = gen2Mon("TESTMON", 60) src.hp = math.floor(src.maxHp / 2) local out = Convert.toGen1(src, gen2Data, gen1Data) T.eq(out.hp, math.floor(src.hp * out.stats.hp / src.maxHp + 0.5), "2->1 scales HP proportionally") end do local src = gen2Mon("TESTMON", 30) src.moves = { { id = "TACKLE", pp = 40, maxPp = 42 } } local out = Convert.toGen1(src, gen2Data, gen1Data) T.eq(out.moves[1].ppUps, 1, "2->1 reads one PP Up out of maxPp") T.eq(out.moves[1].pp, 40, "2->1 keeps current PP under the Gen 1 cap") end -------------------------------------------------------------------------- -- Round trip -------------------------------------------------------------------------- do local src = gen1Mon("TESTMON", 44) local mid = Convert.toGen2(src, gen1Data, gen2Data) local back = Convert.toGen1(mid, gen2Data, gen1Data) T.check(back ~= nil, "round trip comes back") T.eq(back.level, src.level, "round trip keeps the level") T.eq(back.exp, src.exp, "round trip keeps the exp") T.eq(back.catchRate, src.catchRate, "round trip keeps the catch rate") for _, key in ipairs({ "hp", "attack", "defense", "speed", "special" }) do T.eq(back.stats[key], src.stats[key], "round trip keeps stat " .. key) T.eq(back.dvs[key], src.dvs[key], "round trip keeps DV " .. key) end T.eq(back.hp, src.hp, "round trip keeps current HP") T.eq(#back.moves, #src.moves, "round trip keeps the move count") for i, mv in ipairs(src.moves) do T.eq(back.moves[i].id, mv.id, "round trip keeps move " .. i) T.eq(back.moves[i].pp, mv.pp, "round trip keeps PP " .. i) T.eq(back.moves[i].ppUps, mv.ppUps, "round trip keeps PP Ups " .. i) end T.eq(back.nickname, src.nickname, "round trip keeps the nickname") T.eq(back.ot, src.ot, "round trip keeps the OT") T.eq(back.otId, src.otId, "round trip keeps the OT id") end -------------------------------------------------------------------------- -- ValidateOTTrademon -------------------------------------------------------------------------- do local ok = Convert.validateArrival( { species = "TESTMON", level = 30, types = { "GRASS", "POISON" } }, gen1Data) T.check(ok, "arrival accepts matching types") local bad, why = Convert.validateArrival( { species = "TESTMON", level = 30, types = { "FIRE" } }, gen1Data) T.check(not bad, "arrival refuses a type disagreement") T.eq(why, "types", "arrival names the type disagreement") local mag = Convert.validateArrival( { species = "MAGNEMITE", level = 30, types = { "ELECTRIC" } }, { pokemon = { MAGNEMITE = { types = { "ELECTRIC", "STEEL" } } } }) T.check(mag, "arrival carves out MAGNEMITE") local magneton = Convert.validateArrival( { species = "MAGNETON", level = 30, types = { "ELECTRIC" } }, { pokemon = { MAGNETON = { types = { "ELECTRIC", "STEEL" } } } }) T.check(magneton, "arrival carves out MAGNETON") local other = Convert.validateArrival( { species = "MAGNETIC", level = 30, types = { "ELECTRIC" } }, gen2Data) T.check(not other, "arrival refuses another mon claiming the wrong types") T.check(not Convert.validateArrival( { species = "TESTMON", level = 255 }, gen1Data), "arrival refuses a level past 100") T.check(not Convert.validateArrival( { species = "NOSUCHMON", level = 5 }, gen1Data), "arrival refuses an unknown species") T.check(Convert.validateArrival({ species = "TESTMON", level = 30 }, gen1Data), "arrival accepts a record that claims no types") end -------------------------------------------------------------------------- -- Previews -------------------------------------------------------------------------- do local lines, ok = Convert.preview(gen2Mon("TESTMON", 30, { item = "LEFTOVERS" }), 2, 1, gen2Data, gen1Data) T.check(ok, "preview of a legal 2->1 mon is ok") T.check(has(lines, "HELD ITEM LOST: LEFTOVERS"), "preview names the lost held item") local bad, badOk = Convert.preview(gen2Mon("JOHTOMON", 30), 2, 1, gen2Data, gen1Data) T.check(not badOk, "preview of a Johto mon is not ok") T.eq(bad[1], "SPECIES NOT IN GEN 1: JOHTOMON", "preview spells the species refusal") local moveMon = gen2Mon("TESTMON", 30) moveMon.moves = { { id = "SHADOW_BALL", pp = 15, maxPp = 15 } } local mv, mvOk = Convert.preview(moveMon, 2, 1, gen2Data, gen1Data) T.check(not mvOk, "preview of an illegal move is not ok") T.eq(mv[1], "MOVE ILLEGAL IN GEN 1: SHADOW BALL", "preview spells the move refusal") local mailMon = gen2Mon("TESTMON", 30, { item = "FLOWER_MAIL" }) local ml, mlOk = Convert.preview(mailMon, 2, 1, gen2Data, gen1Data) T.check(not mlOk, "preview of a mail holder is not ok") T.eq(ml[1], "MON IS HOLDING MAIL", "preview spells the mail refusal") local eggLines = Convert.preview(gen2Mon("TESTMON", 5, { isEgg = true }), 2, 1, gen2Data, gen1Data) T.eq(eggLines[1], "EGGS CANNOT TRAVEL", "preview spells the egg refusal") local splitSrc = gen1Mon("TESTMON", 50) splitSrc.species = "SPLITMON" splitSrc.stats = Stats.calc( { baseStats = { hp = 60, attack = 60, defense = 60, speed = 60, special = 65 } }, 50, splitSrc.dvs, ZERO_EXP) splitSrc.hp = splitSrc.stats.hp local up, upOk = Convert.preview(splitSrc, 1, 2, gen1Data, gen2Data) T.check(upOk, "preview of a 1->2 mon is ok") T.check(has(up, "SPECIAL SPLIT "), "preview shows the Special split") end -------------------------------------------------------------------------- -- Party mapping with mixed legality -------------------------------------------------------------------------- do local illegalMove = gen2Mon("TESTMON", 30) illegalMove.moves = { { id = "SHADOW_BALL", pp = 15, maxPp = 15 } } local party = { gen2Mon("TESTMON", 30), gen2Mon("JOHTOMON", 30), illegalMove, gen2Mon("MAGNETIC", 30, { item = "METAL_COAT" }), } local converted, results = Convert.partyToGen1(party, gen2Data, gen1Data) T.eq(#converted, 2, "party mapping converts only the legal mons") T.eq(converted[1].species, "TESTMON", "party mapping keeps order") T.eq(converted[2].species, "MAGNETIC", "party mapping keeps the second") T.eq(#results, 4, "party mapping reports every slot") T.check(results[1].ok, "slot 1 is legal") T.check(not results[2].ok, "slot 2 is refused") T.eq(results[2].reason, "species_too_new", "slot 2 refusal reason") T.eq(results[2].index, 2, "slot 2 carries its index") T.eq(results[3].reason, "move_too_new", "slot 3 refusal reason") T.eq(results[3].info.move, "SHADOW_BALL", "slot 3 names the move") T.check(results[4].ok, "slot 4 is legal with the item dropped") T.check(has(results[4].preview, "HELD ITEM LOST: METAL COAT"), "slot 4 preview names the dropped METAL COAT") T.eq(#Convert.refusals(results), 2, "two refusals collected") local up, upResults = Convert.partyToGen2( { gen1Mon("TESTMON", 10), gen1Mon("MAGNETIC", 12) }, gen1Data, gen2Data) T.eq(#up, 2, "every Kanto mon is legal going up") T.check(upResults[1].ok and upResults[2].ok, "no refusals going up") T.eq(#Convert.refusals(upResults), 0, "no refusals collected going up") end -------------------------------------------------------------------------- -- Real data -------------------------------------------------------------------------- local function realDataset(version) local roots = { ("%s/data/generated/"):format(version), } if version == "red" then roots[#roots + 1] = "data/generated/" end for _, root in ipairs(roots) do local probe = io.open(root .. "pokemon.lua", "r") if probe then probe:close() local function load(name) local chunk = loadfile(root .. name .. ".lua") if not chunk then return nil end local ok, value = pcall(chunk) return ok and value or nil end local data = { pokemon = load("pokemon"), moves = load("moves"), items = load("items") } if data.pokemon and data.moves then return data end end end return nil end local red = realDataset("red") local gold = realDataset("gold") if red and gold then local function goldMon(species, level, over) local mon = Mon.new(gold, species, level, { dvs = { attack = 15, defense = 14, speed = 13, special = 12 } }) for k, v in pairs(over or {}) do mon[k] = v end return mon end local bulb = goldMon("BULBASAUR", 30) local down = Convert.toGen1(bulb, gold, red) T.check(down ~= nil, "real BULBASAUR converts down") T.eq(down.catchRate, red.pokemon.BULBASAUR.catchRate, "real BULBASAUR takes Red's catch rate") local magnemite = goldMon("MAGNEMITE", 30) local mDown = Convert.toGen1(magnemite, gold, red) T.check(mDown ~= nil, "real MAGNEMITE converts down") T.check(Convert.validateArrival( { species = "MAGNEMITE", level = 30, types = red.pokemon.MAGNEMITE.types }, gold), "real MAGNEMITE survives arrival validation despite the STEEL type") local scyther = goldMon("SCYTHER", 40, { item = "METAL_COAT" }) local sDown, sReport = Convert.toGen1(scyther, gold, red) T.check(sDown ~= nil, "real SCYTHER with METAL COAT converts down") T.check(kinds(sReport.lost).item ~= nil, "real METAL COAT is reported lost") local totodile = goldMon("TOTODILE", 15) local tDown, tReason = Convert.toGen1(totodile, gold, red) T.check(tDown == nil and tReason == "species_too_new", "real TOTODILE is refused as a Johto species") local shadow = goldMon("GASTLY", 40) shadow.moves = { { id = "SHADOW_BALL", pp = 15, maxPp = 15 } } local shDown, shReason = Convert.toGen1(shadow, gold, red) T.check(shDown == nil and shReason == "move_too_new", "real SHADOW BALL is refused") local mailed = goldMon("BULBASAUR", 30, { item = "FLOWER_MAIL" }) local mlDown, mlReason = Convert.toGen1(mailed, gold, red) T.check(mlDown == nil and mlReason == "has_mail", "real FLOWER MAIL is refused") local upBulb = Convert.toGen2(down, red, gold) T.check(upBulb ~= nil, "real BULBASAUR converts back up") T.eq(upBulb.level, bulb.level, "real round trip keeps the level") else T.check(true, "real-data cases skipped (no red and gold caches)") end T.finish("online_convert")