mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-09 01:59:15 -04:00
376 lines
17 KiB
Lua
376 lines
17 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
love = love or require("tests.love_stub")
|
|
local T = require("tests.harness")
|
|
local F = require("tests.engine._xgen_fixture")
|
|
local P = require("tests.engine._union_prep_pair")
|
|
local Model = require("src.online.union.BattlePrepModel")
|
|
local Rentals = require("src.online.xgen.Rentals")
|
|
|
|
local emerald = F.data("emerald")
|
|
local red = F.data("red")
|
|
|
|
local savedDefs = Rentals.DEFINITIONS["g3u-gen1"]
|
|
Rentals.DEFINITIONS["g3u-gen1"] = {
|
|
{ type = "GRASS", species = 1, moves = { 33, 45 } },
|
|
{ type = "ELECTRIC", species = 25, moves = { 84, 45 } },
|
|
{ type = "PSYCHIC", species = 150, moves = { 94 } },
|
|
}
|
|
|
|
local function mon3(national, level, moves, extra)
|
|
local m = { species = emerald.nationalToLocal[national], level = level, personality = 7, otId = 1, otSecretId = 0,
|
|
otName = "MAY", ivs = { hp = 1, atk = 1, def = 1, spe = 1, spa = 1, spd = 1 }, evs = {}, moves = moves }
|
|
for k, v in pairs(extra or {}) do m[k] = v end
|
|
return m
|
|
end
|
|
|
|
local function source()
|
|
return {
|
|
generation = 3,
|
|
party = { mon3(252, 20, { 33, 345 }), mon3(25, 20, { 84, 45, 98, 57 }, { ppBonuses = 2 }), mon3(1, 10, { 33 }) },
|
|
boxes = { { mon3(197, 30, { 33 }), mon3(25, 18, { 84 }), mon3(152, 25, { 33 }), mon3(1, 22, { 33 }),
|
|
mon3(2, 21, { 33 }), mon3(150, 60, { 94 }) } },
|
|
}
|
|
end
|
|
|
|
local function labels(pg)
|
|
local out = {}
|
|
for _, it in ipairs(pg.items) do out[#out + 1] = it.label end
|
|
return out
|
|
end
|
|
|
|
local function find(pg, id, label)
|
|
for i, it in ipairs(pg.items) do
|
|
if it.id == id and (label == nil or it.label:find(label, 1, true)) then return i, it end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function pick(m, id, label)
|
|
local pg = m:page()
|
|
local i = find(pg, id, label)
|
|
assert(i, "no item " .. id .. " " .. tostring(label) .. " on " .. tostring(pg.step) .. ": " .. table.concat(labels(pg), ","))
|
|
m.cursor = i
|
|
m:input("a")
|
|
end
|
|
|
|
local function joined(lines)
|
|
return table.concat(lines or {}, " | ")
|
|
end
|
|
|
|
local function pumpBoth(w, ma, pb)
|
|
for _ = 1, 3 do
|
|
w:pump()
|
|
ma:poll()
|
|
if pb then pb:poll() end
|
|
end
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("emerald", "red")
|
|
local src = source()
|
|
local before = F.copy(src)
|
|
local m = Model.new({ version = "emerald", gen = 3, data = emerald, prep = pa, owned = src,
|
|
opponent = { name = "BOB", version = "red", gen = 1 } })
|
|
pumpBoth(w, m, pb)
|
|
T.eq(m.step, "rules", "the prep opens on the opponent and rules")
|
|
local pg = m:page()
|
|
T.check(joined(pg.lines):find("BOB wants to battle", 1, true) ~= nil, "the rules page names the opponent")
|
|
T.check(joined(pg.lines):find("Pokémon Red", 1, true) ~= nil, "the rules page names the opponent's game")
|
|
T.check(joined(pg.lines):find("No. 1 to 151", 1, true) ~= nil, "the rules page shows the dex limit")
|
|
T.check(find(pg, "changes") ~= nil and find(pg, "rentals") ~= nil, "rules and rentals can be opened")
|
|
|
|
pick(m, "changes")
|
|
local chg = joined(m:page().lines)
|
|
T.check(chg:find("Held items are off", 1, true) and chg:find("Abilities are off", 1, true)
|
|
and chg:find("Natures are neutral", 1, true) and chg:find("Gen 1 ones", 1, true)
|
|
and chg:find("IVs and EVs stay", 1, true), "the rules view lists items, abilities, natures, typing and IV policy")
|
|
m:input("b")
|
|
T.eq(m.view, nil, "B closes the rules view")
|
|
|
|
pick(m, "rentals")
|
|
local rp = m:page()
|
|
T.eq(#rp.items, 4, "three valid rentals plus back")
|
|
T.check(rp.items[1].label:find("GRASS BULBASAUR", 1, true) ~= nil, "rentals are listed by type")
|
|
local rinfo = joined(rp.info)
|
|
T.check(rinfo:find("RENTAL", 1, true) and rinfo:find("Lv50", 1, true) and rinfo:find("HP ", 1, true)
|
|
and rinfo:find("TACKLE 35/35", 1, true), "a rental discloses its tag, level, stats and moves")
|
|
m:input("b")
|
|
|
|
pick(m, "continue")
|
|
T.eq(m.step, "problems", "continue moves to team compatibility")
|
|
local probs = joined(m:page().lines)
|
|
T.check(probs:find("TREECKO can't join this battle. Only No. 1 to 151 can.", 1, true) ~= nil, "Treecko's block is explained")
|
|
T.check(probs:find("PIKACHU can't learn SURF", 1, true) ~= nil, "Pikachu's Surf is explained as not learnable")
|
|
|
|
pick(m, "continue")
|
|
T.eq(m.step, "substitute", "the substitution step follows")
|
|
local sp = m:page()
|
|
T.check(sp.items[1].label:find("IVYSAUR", 1, true) and sp.items[2].label:find("BULBASAUR", 1, true),
|
|
"owned grass types rank first, closest level first (" .. joined(labels(sp)) .. ")")
|
|
local again = m:page()
|
|
T.eq(joined(labels(again)), joined(labels(sp)), "the ranking is stable across rebuilds")
|
|
T.check(joined(sp.items[1].info or sp.items[1].detail):find("Shares a type", 1, true) ~= nil, "the ranking reason is shown")
|
|
local sawChikorita = false
|
|
for _, it in ipairs(sp.items) do if it.label:find("CHIKORITA", 1, true) then sawChikorita = true end end
|
|
T.check(not sawChikorita, "species outside the dex are never offered")
|
|
T.check(find(sp, "swap_rental", "RENTAL BULBASAUR") ~= nil, "rentals are offered next to owned Pokemon")
|
|
T.check(find(sp, "leave") ~= nil, "a slot can be left out")
|
|
pick(m, "swap_rental", "RENTAL BULBASAUR")
|
|
|
|
T.eq(m.step, "moves", "move adjustments follow substitutions")
|
|
local mp = m:page()
|
|
T.check(joined(mp.lines):find("PIKACHU can't learn SURF", 1, true) ~= nil, "the move problem is explained by itself")
|
|
T.check(joined(mp.lines):find("PIKACHU: PIKACHU", 1, true) == nil, "the name is not repeated")
|
|
T.eq(mp.items[1].label, "THUNDERBOLT", "a similar legal move is suggested first")
|
|
for _, it in ipairs(mp.items) do
|
|
T.check(it.arg ~= 84 and it.arg ~= 45 and it.arg ~= 98, "a move the mon already knows is not suggested: " .. it.label)
|
|
end
|
|
local _, empty = find(mp, "empty")
|
|
T.check(empty and not empty.disabled, "an empty slot is allowed when other legal moves remain")
|
|
pick(m, "move", "THUNDERBOLT")
|
|
|
|
T.eq(m.step, "size", "team size follows moves")
|
|
pumpBoth(w, m, pb)
|
|
T.eq(pa.mine.roster and pa.mine.roster.size, 3, "the roster size reaches the relay")
|
|
pb:roster(2, "00000000000000b2")
|
|
pumpBoth(w, m, pb)
|
|
T.eq(pa.size, 2, "the relay agrees the smaller size")
|
|
local zp = m:page()
|
|
local _, cont = find(zp, "continue")
|
|
T.check(cont and cont.disabled, "the larger roster must choose who sits out before continuing")
|
|
T.check(joined(zp.lines):find("Choose 1 to sit out", 1, true) ~= nil, "the sit-out count is shown")
|
|
T.check(joined(zp.lines):find("BOB brings 2", 1, true) ~= nil, "the opponent's size is shown")
|
|
m.cursor = find(zp, "continue")
|
|
m:input("a")
|
|
T.eq(m.step, "size", "a disabled continue does nothing")
|
|
local toggles = 0
|
|
for _, it in ipairs(zp.items) do if it.id == "toggle" then toggles = toggles + 1 end end
|
|
T.eq(toggles, 3, "every battler can be chosen to sit out")
|
|
pick(m, "toggle", "PIKACHU")
|
|
pick(m, "continue")
|
|
T.eq(m.step, "confirm", "final confirmation follows the size")
|
|
local cp = m:page()
|
|
T.eq(#cp.items, 6, "two battlers, confirm, rules, back, cancel")
|
|
T.check(cp.items[1].label:find("RENTAL BULBASAUR", 1, true) ~= nil, "a rental is clearly marked")
|
|
local ready = select(2, find(cp, "ready"))
|
|
T.check(ready and not ready.disabled, "confirm is available")
|
|
pick(m, "ready")
|
|
T.eq(m.step, "waiting", "confirm waits for the opponent")
|
|
T.check(pa.mine.ready ~= nil, "ready is sent to the relay")
|
|
pumpBoth(w, m, pb)
|
|
|
|
pb:roster(2, "00000000000000c3")
|
|
pumpBoth(w, m, pb)
|
|
T.eq(m.step, "confirm", "a later roster change sends the player back to confirm")
|
|
T.check(joined(m:page().lines):find("Something changed", 1, true) ~= nil, "the invalidation is explained")
|
|
T.eq(pa.mine.ready, nil, "readiness is cleared")
|
|
pick(m, "ready")
|
|
pumpBoth(w, m, pb)
|
|
pb:ready("00000000000000c3")
|
|
pumpBoth(w, m, pb)
|
|
T.check(m.done and m.outcome == "go", "both confirmations start the battle")
|
|
local res = m:result()
|
|
T.eq(res and #res.records, 2, "the result carries the chosen team")
|
|
T.eq(res.size, 2, "the result carries the size")
|
|
T.eq(res.ruleset.id, "g3u", "the result carries the ruleset")
|
|
T.eq(res.ruleset.rulesetId, "g3u-gen1", "the result names the policy ruleset")
|
|
T.eq(res.seed, pa.go.seed, "the result carries the relay seed")
|
|
T.check(res.records[1].rental == true and res.records[1].species == 1, "the rental stays flagged in the result")
|
|
T.eq(res.records[2].species, 1, "Bulbasaur fills the second place in party order")
|
|
T.check(F.deepEqual(src, before), "the active save's party and PC are untouched")
|
|
local nick = false
|
|
local function walk(t, seen)
|
|
if type(t) ~= "table" or seen[t] then return end
|
|
seen[t] = true
|
|
for k, v in pairs(t) do
|
|
if k ~= "prep" and k ~= "data" then
|
|
if v == "ZAPZAP" then nick = true end
|
|
walk(v, seen)
|
|
end
|
|
end
|
|
end
|
|
walk(m, {})
|
|
T.check(not nick, "no opponent record is ever in the model")
|
|
for k in pairs(pa.peer) do
|
|
T.check(k == "roster" or k == "sizeReq" or k == "ready" or k == "caps" or k == "capsSent" or k == "counter",
|
|
"the peer view holds sizes and flags only: " .. tostring(k))
|
|
end
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("emerald", "red")
|
|
local src = source()
|
|
local m = Model.new({ version = "emerald", gen = 3, data = emerald, prep = pa, owned = src,
|
|
opponent = { name = "BOB", version = "red", gen = 1 } })
|
|
pumpBoth(w, m, pb)
|
|
pick(m, "continue")
|
|
pick(m, "continue")
|
|
pick(m, "swap_owned", "IVYSAUR")
|
|
pick(m, "empty")
|
|
T.eq(m.step, "size", "an emptied slot with other legal moves passes")
|
|
pumpBoth(w, m, pb)
|
|
pb:roster(3, "00000000000000b2")
|
|
pumpBoth(w, m, pb)
|
|
T.eq(pa.size, 3, "equal rosters agree on 3")
|
|
pb:sizeRequest(1)
|
|
pumpBoth(w, m, pb)
|
|
local zp = m:page()
|
|
T.check(joined(zp.lines):find("BOB asks for 1 each", 1, true) ~= nil, "the opponent's size request is shown")
|
|
T.eq(pa.size, 3, "one request alone does not change the size")
|
|
pick(m, "agree")
|
|
pumpBoth(w, m, pb)
|
|
T.eq(pa.size, 1, "both requests agree on another equal size")
|
|
T.check(joined(m:page().lines):find("Choose 2 to sit out", 1, true) ~= nil, "the agreed size asks for two to sit out")
|
|
pick(m, "toggle", "PIKACHU")
|
|
pick(m, "toggle", "BULBASAUR")
|
|
pick(m, "continue")
|
|
T.eq(m.step, "confirm", "the agreed size reaches confirm")
|
|
local cp = m:page()
|
|
T.check(cp.items[1].label:find("IVYSAUR", 1, true) ~= nil, "the substitute battles alone")
|
|
local detail = joined(cp.items[1].full)
|
|
T.check(detail:find("TACKLE 35/35", 1, true) ~= nil, "the battler's moves and PP are shown")
|
|
T.check(detail:find("IV ", 1, true) and detail:find("EV ", 1, true), "IVs and EVs are disclosed")
|
|
pick(m, "cancel")
|
|
T.eq(m.step, "closed", "cancel closes the prep")
|
|
T.eq(#m.team, 0, "cancel discards the temporary team")
|
|
T.eq(m:result(), nil, "a cancelled prep has no result")
|
|
pumpBoth(w, m, pb)
|
|
T.eq(pb.state, "closed", "the opponent's prep closes")
|
|
m:input("a")
|
|
T.check(m.done and m.outcome == "cancel", "OK finishes a closed prep")
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("emerald", "red")
|
|
local src = source()
|
|
src.party[2] = mon3(25, 20, { 84, 45, 98, 57 }, { ppBonuses = 2 })
|
|
src.party[3] = mon3(1, 10, { 345 })
|
|
local m = Model.new({ version = "emerald", gen = 3, data = emerald, prep = pa, owned = src,
|
|
opponent = { name = "BOB" } })
|
|
pumpBoth(w, m, pb)
|
|
pick(m, "continue")
|
|
local probs = joined(m:page().lines)
|
|
T.check(probs:find("MAGICAL LEAF isn't used in this battle", 1, true) ~= nil, "a move outside the ruleset is told apart")
|
|
T.check(probs:find("can't learn SURF", 1, true) ~= nil, "from a move the species can't learn")
|
|
pick(m, "continue")
|
|
pick(m, "leave")
|
|
T.eq(m.step, "moves", "leaving Treecko out goes to moves")
|
|
pick(m, "move", "THUNDERBOLT")
|
|
local mp = m:page()
|
|
T.check(joined(mp.lines):find("BULBASAUR: MAGICAL LEAF", 1, true) ~= nil, "each illegal move gets its own screen")
|
|
local _, empty = find(mp, "empty")
|
|
T.check(empty and empty.disabled, "the only move can't be emptied")
|
|
m.cursor = find(mp, "empty")
|
|
m:input("a")
|
|
T.eq(m.step, "moves", "an empty choice that leaves no move is refused")
|
|
T.check(joined(m:page().lines):find("needs at least one move", 1, true) ~= nil, "the refusal is explained")
|
|
local legal = {}
|
|
for _, it in ipairs(mp.items) do if it.id == "move" then legal[#legal + 1] = it.arg end end
|
|
for _, id in ipairs(legal) do
|
|
T.check(id == 33 or id == 45 or id == 22 or id == 34 or id == 92 or id == 75, "replacement is legal for Bulbasaur at 10: " .. id)
|
|
end
|
|
pick(m, "move", "TACKLE")
|
|
T.eq(m.step, "size", "all moves resolved")
|
|
pumpBoth(w, m, pb)
|
|
pb:roster(2, "00000000000000b2")
|
|
pumpBoth(w, m, pb)
|
|
pick(m, "continue")
|
|
T.eq(m.step, "confirm", "equal sizes need no sit-out")
|
|
local r = m:finalReport()
|
|
T.check(r.ok, "the final team is legal")
|
|
local pika = r.result.team[1]
|
|
T.eq(pika.moves[1].ppUps, 2, "kept moves keep their PP Ups")
|
|
T.eq(pika.moves[1].pp, 42, "kept move PP includes PP Ups (30 + 2 x 6)")
|
|
T.eq(pika.moves[4].id, 85, "the replacement stays in the slot it replaced")
|
|
T.eq(pika.moves[4].pp, 15, "the replacement starts at its full base PP")
|
|
T.eq(pika.moves[4].ppUps, 0, "the replacement has no PP Ups")
|
|
T.eq(#r.result.team[2].moves, 1, "Bulbasaur has its one legal move")
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("red", "emerald")
|
|
local party = {
|
|
{ species = "PIKACHU", level = 20, dvs = { attack = 9, defense = 8, speed = 7, special = 6 },
|
|
statExp = { hp = 100, attack = 400, defense = 0, speed = 0, special = 900 },
|
|
moves = { { id = "THUNDERSHOCK", pp = 30 }, { id = "GROWL", pp = 40 } } },
|
|
}
|
|
local m = Model.new({ version = "red", gen = 1, data = red, prep = pa, owned = { party = party, generation = 1 },
|
|
opponent = { name = "BOB" } })
|
|
pumpBoth(w, m, pb)
|
|
pick(m, "changes")
|
|
local chg = joined(m:page().lines)
|
|
T.check(chg:find("Special counts as both", 1, true) and chg:find("DVs become IVs", 1, true)
|
|
and chg:find("Stat Exp. becomes EVs", 1, true), "a Gen 1 player sees the Special, DV and Stat Exp. policy")
|
|
m:input("b")
|
|
local gb = m:formatLines({ "Every Pokémon can battle." })
|
|
T.eq(gb[1], "EVERY POKéMON CAN", "Gen 1 text is upper case and wrapped to 18")
|
|
end
|
|
|
|
do
|
|
local m = Model.new({ version = "emerald", gen = 3, data = emerald, owned = source(), gameplayMods = true,
|
|
rules = { ruleset = "g3u", dexMax = 151, moveMax = 165, moveGen = 1, gens = { 1, 3 } } })
|
|
local pg = m:page()
|
|
T.check(joined(pg.lines):find("Gameplay mods are on", 1, true) ~= nil, "gameplay mods refuse g3u with an explanation")
|
|
T.eq(#pg.items, 1, "only cancel is offered")
|
|
local n = Model.new({ version = "emerald", gen = 3, data = emerald, owned = source(), gameplayMods = true,
|
|
rules = { ruleset = "native", gen = 3, gens = { 3, 3 } } })
|
|
T.check(joined(n:page().lines):find("Gameplay mods are on", 1, true) ~= nil, "gameplay mods refuse native battles too")
|
|
local d = Model.new({ version = "emerald", gen = 3, data = false, owned = source(),
|
|
rules = { ruleset = "g3u", dexMax = 151, moveMax = 165, moveGen = 1, gens = { 1, 3 } } })
|
|
T.check(joined(d:page().lines):find("Import it again", 1, true) ~= nil, "a missing dataset gives actionable text")
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("red", "red")
|
|
local m = Model.new({ version = "red", gen = 1, data = red, prep = pa, owned = { party = {
|
|
{ species = "PIKACHU", level = 20, dvs = { attack = 9, defense = 8, speed = 7, special = 6 }, statExp = {},
|
|
moves = { { id = "THUNDERSHOCK", pp = 30 } } } }, generation = 1 }, opponent = { name = "BOB" } })
|
|
pumpBoth(w, m, pb)
|
|
local pg = m:page()
|
|
T.check(joined(pg.lines):find("rules of Gen 1", 1, true) ~= nil, "same-gen pairs use the native rules")
|
|
T.eq(find(pg, "rentals"), nil, "native battles have no rentals")
|
|
pick(m, "continue")
|
|
pick(m, "continue")
|
|
T.eq(m.step, "size", "a native team needs no substitutions or moves")
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("emerald", "red")
|
|
pb:cancel("bye")
|
|
local m = Model.new({ version = "emerald", gen = 3, data = emerald, prep = pa, owned = source(),
|
|
opponent = { name = "BOB" } })
|
|
pumpBoth(w, m, pb)
|
|
T.eq(m.step, "closed", "a peer cancel closes the prep")
|
|
T.check(joined(m:page().lines):find("BOB cancelled", 1, true) ~= nil, "the close names the opponent")
|
|
T.eq(#m.team, 0, "the temporary team is discarded")
|
|
end
|
|
|
|
do
|
|
local w, pa, pb = P.pair("emerald", "gold")
|
|
local gold = F.data("gold")
|
|
local party = { { species = "CHIKORITA", level = 12, dvs = { attack = 9, defense = 8, speed = 7, special = 6 },
|
|
statExp = {}, moves = { { id = "TACKLE", pp = 35 } } } }
|
|
local mb = Model.new({ version = "gold", gen = 2, data = gold, prep = pb, owned = { party = party, generation = 2 },
|
|
opponent = { name = "ALICE" } })
|
|
local m = Model.new({ version = "emerald", gen = 3, data = emerald, prep = pa, owned = source(),
|
|
opponent = { name = "BOB" } })
|
|
pumpBoth(w, m, nil)
|
|
mb:poll()
|
|
T.eq(pa.rules.dexMax, 251, "Gen 2 vs Gen 3 starts at dex 251")
|
|
pick(m, "continue")
|
|
pick(m, "continue")
|
|
T.eq(m.step, "substitute", "Treecko (No. 252) is outside dex 251")
|
|
pb:counter(1)
|
|
pumpBoth(w, m, nil)
|
|
mb:poll()
|
|
T.eq(pa.rules.dexMax, 151, "the counter narrows the rules")
|
|
T.eq(m.step, "rules", "a rules change sends the player back to the rules")
|
|
T.check(joined(m:page().lines):find("The rules changed", 1, true) ~= nil, "the rules change is explained")
|
|
T.eq(mb.step, "rules", "the proposing side restarts too")
|
|
pick(mb, "continue")
|
|
T.check(joined(mb:page().lines):find("CHIKORITA can't join", 1, true) ~= nil, "the new rules re-check the team")
|
|
end
|
|
|
|
Rentals.DEFINITIONS["g3u-gen1"] = savedDefs
|
|
T.finish()
|