mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 17:51:37 -04:00
288 lines
14 KiB
Lua
288 lines
14 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._g3u_fixture")
|
|
local L = require("tests.support.g3u_loopback")
|
|
local Table = require("src.battle.g3u.Table")
|
|
local BattleSession = require("src.online.union.BattleSession")
|
|
local Battle = require("src.battle.gen2.Battle")
|
|
local Effects = require("src.battle.gen2.Effects")
|
|
local Strings = require("src.core.Strings")
|
|
|
|
local okLoad, Gen2Facade = pcall(require, "src.ui.g3u.Gen2Facade")
|
|
T.check(okLoad, "src.ui.g3u.Gen2Facade loads: " .. tostring(not okLoad and Gen2Facade or ""))
|
|
if not okLoad then T.finish("g3u gen2 facade") return end
|
|
|
|
local function readFile(path)
|
|
local f = assert(io.open(path, "rb"))
|
|
local s = f:read("*a")
|
|
f:close()
|
|
return s
|
|
end
|
|
|
|
local function uiMembers()
|
|
local src = readFile("src/ui/gen2/BattleState.lua")
|
|
src = src:gsub("%-%-%[%[.-%]%]", "")
|
|
local lines = {}
|
|
for raw in (src .. "\n"):gmatch("([^\n]*)\n") do
|
|
local line = raw:gsub('"[^"]*"', '""'):gsub("'[^']*'", "''"):gsub("%-%-.*$", "")
|
|
lines[#lines + 1] = line
|
|
end
|
|
src = table.concat(lines, "\n")
|
|
local methods, fields, hooks = {}, {}, {}
|
|
for name in src:gmatch("%f[%w_]battle:([%a_][%w_]*)%s*%(") do methods[name] = true end
|
|
for name in src:gmatch("%f[%w_]battle%.([%a_][%w_]*)") do fields[name] = true end
|
|
for name in src:gmatch("self%.link%.([%a_][%w_]*)") do hooks[name] = true end
|
|
return methods, fields, hooks
|
|
end
|
|
|
|
local function dataFixture()
|
|
local pokemon, moves = {}, {}
|
|
for n = 1, 251 do
|
|
pokemon[("MON%03d"):format(n)] = { dex = n, name = ("MON%03d"):format(n), types = { "NORMAL" } }
|
|
end
|
|
for id = 1, 251 do
|
|
moves[("MOVE%03d"):format(id)] = { index = id, name = ("MOVE%03d"):format(id) }
|
|
end
|
|
pokemon.growthRates = {}
|
|
return { pokemon = pokemon, moves = moves }
|
|
end
|
|
|
|
local game = { data = dataFixture(), save = { player = { name = "GOLD" } } }
|
|
local t2 = assert(Table.build(F.gen2(), 2))
|
|
|
|
local function pair(p0, p1, seed)
|
|
local netA, netB = L.pair()
|
|
local go = { seed = seed or 11, size = 6 }
|
|
local gens = { [0] = 2, [1] = 3 }
|
|
local names = { [0] = "GOLD", [1] = "MAY" }
|
|
local me = BattleSession.new({ net = netA, seat = 0, go = go, gens = gens, data = F.gen2(), records = p0,
|
|
names = names })
|
|
local bot = BattleSession.new({ net = netB, seat = 1, go = go, gens = gens, records = p1, names = names })
|
|
return me, bot
|
|
end
|
|
|
|
local function pumpBoth(me, bot, facade, n)
|
|
local out = {}
|
|
for _ = 1, n or 4 do
|
|
me:update()
|
|
bot:update()
|
|
for _, e in ipairs(facade:translate(me:events())) do out[#out + 1] = e end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function find(list, pred)
|
|
for _, e in ipairs(list) do if pred(e) then return e end end
|
|
return nil
|
|
end
|
|
|
|
do
|
|
local methods, fields, hooks = uiMembers()
|
|
local nM, nF = 0, 0
|
|
local listed = {}
|
|
for _, k in ipairs(Gen2Facade.FIELDS) do listed[k] = true end
|
|
local mine = { F.record(t2, 25, { 33, 85 }), F.record(t2, 1, { 33 }) }
|
|
local foe = { F.record(t2, 4, { 33 }) }
|
|
local me, bot = pair(mine, foe)
|
|
local facade = Gen2Facade.new(game, me, { names = { me = "GOLD", foe = "MAY" } })
|
|
pumpBoth(me, bot, facade)
|
|
for name in pairs(methods) do
|
|
nM = nM + 1
|
|
T.check(type(facade[name]) == "function", "facade method battle:" .. name .. " exists")
|
|
end
|
|
for _, name in ipairs(Gen2Facade.METHODS) do
|
|
T.check(type(facade[name]) == "function", "declared method " .. name .. " is a function")
|
|
end
|
|
for name in pairs(fields) do
|
|
nF = nF + 1
|
|
T.check(listed[name] == true, "UI field battle." .. name .. " is in Gen2Facade.FIELDS")
|
|
end
|
|
for _, name in ipairs(Gen2Facade.FIELDS) do
|
|
T.check(facade[name] ~= nil or Gen2Facade.NILABLE[name], "field " .. name .. " is set")
|
|
end
|
|
T.check(nM >= 15 and nF >= 12, ("grep found the UI members (%d methods, %d fields)"):format(nM, nF))
|
|
T.check(type(facade.player) == "table" and type(facade.enemy) == "table", "player and enemy are views")
|
|
T.check(type(facade.random) == "function", "random is a function")
|
|
T.check(type(facade.trainer) == "table" and facade.trainer.name == "MAY", "trainer carries the foe name")
|
|
local h = facade:hooks()
|
|
for name in pairs(hooks) do
|
|
if name ~= "forcedPrompt" then
|
|
T.check(type(h[name]) == "function", "link hook " .. name .. " exists")
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
local mine = { F.record(t2, 25, { 33, 85 }, 50, { nickname = "SPARKY" }), F.record(t2, 1, { 33 }) }
|
|
local foe = { F.record(t2, 4, { 33 }), F.record(t2, 7, { 33 }) }
|
|
local me, bot = pair(mine, foe)
|
|
local facade = Gen2Facade.new(game, me, { names = { me = "GOLD", foe = "MAY" } })
|
|
pumpBoth(me, bot, facade)
|
|
T.check(facade.ready, "ready builds the views")
|
|
T.eq(#facade.party, 2, "my party has both mons")
|
|
T.eq(#facade.enemyParty, 2, "foe party has both mons")
|
|
T.eq(facade.player.species, "MON025", "species key from the active cache by national dex")
|
|
T.eq(facade.player.nickname, "SPARKY", "nickname from the record")
|
|
T.eq(facade.enemy.nickname, "MON004", "no nickname: species name from the active cache")
|
|
T.eq(facade.player.moves[1].id, "MOVE033", "move ids map to the active cache keys")
|
|
T.eq(facade.awaiting, "move", "the first prompt arms the move menu")
|
|
T.check(facade:hasUsableMoves(facade.player), "usable moves while choosing")
|
|
local legalSlots = {}
|
|
for _, a in ipairs(me:legal()) do if a.kind == "move" then legalSlots[a.slot] = true end end
|
|
for i, mv in ipairs(facade.player.moves) do
|
|
T.eq(facade:moveDisabled(facade.player, mv.id), not legalSlots[i], "move " .. i .. " disabled iff not legal")
|
|
end
|
|
T.check(facade:moveDisabled(facade.player, "MOVE099"), "a move the mon does not have is refused")
|
|
T.eq(facade:switchLocked(), false, "switching is open with a bench")
|
|
T.eq(facade:lockedInMove(facade.player), nil, "nothing is locked in")
|
|
|
|
local refused
|
|
local s = { phase = "moves", queue = {} }
|
|
function s.refuseMenu(_, text) refused = text end
|
|
function s.refuseSwitch() refused = "switch" end
|
|
local h = facade:hooks()
|
|
h.menuChoice(s, "item")
|
|
T.check(refused ~= nil, "items are refused")
|
|
refused = nil
|
|
T.eq(h.menuChoice(s, "run"), true, "first RUN only warns")
|
|
T.eq(me.phase, "choose", "first RUN does not forfeit")
|
|
T.eq(h.menuChoice(s, "fight"), false, "FIGHT falls through to the native menu")
|
|
h.submit(s, { kind = "move", move = facade.player.moves[1].id })
|
|
T.eq(me.phase, "wait", "the move menu submits the legal slot")
|
|
T.eq(s.phase, "link-wait", "the screen waits for the peer")
|
|
local bstep = L.bot(bot, { seed = 3 })
|
|
local out = {}
|
|
for _ = 1, 6 do
|
|
bstep()
|
|
me:update()
|
|
for _, e in ipairs(facade:translate(me:events())) do out[#out + 1] = e end
|
|
end
|
|
local mv = find(out, function(e) return e.kind == "move" and e.side == "player" end)
|
|
T.check(mv and mv.move == "MOVE033" and type(mv.text) == "string" and mv.text:find("SPARKY"),
|
|
"USEDMOVE + move merge into one Gen 2 move event with text")
|
|
T.check(find(out, function(e) return e.kind == "damage" and e.side == "enemy" and e.hp ~= nil end) ~= nil,
|
|
"a hit becomes a damage event with the new hp")
|
|
T.eq(facade.awaiting, "move", "the next prompt arms the menu again")
|
|
end
|
|
|
|
do
|
|
local mine = { F.record(t2, 25, { 33 }, 5), F.record(t2, 1, { 33 }) }
|
|
local foe = { F.record(t2, 143, { 33 }, 100) }
|
|
local me, bot = pair(mine, foe, 5)
|
|
local facade = Gen2Facade.new(game, me, {})
|
|
pumpBoth(me, bot, facade)
|
|
local s = { phase = "moves", queue = {} }
|
|
local refused
|
|
function s.refuseMenu(_, text) refused = text end
|
|
function s.refuseSwitch() refused = "switch" end
|
|
local h = facade:hooks()
|
|
h.submit(s, { kind = "move", move = facade.player.moves[1].id })
|
|
local bstep = L.bot(bot, { seed = 1 })
|
|
local out = {}
|
|
for _ = 1, 6 do
|
|
bstep()
|
|
me:update()
|
|
for _, e in ipairs(facade:translate(me:events())) do out[#out + 1] = e end
|
|
end
|
|
local fe = find(out, function(e) return e.kind == "faint" and e.side == "player" end)
|
|
T.check(fe and fe.text:find("fainted"), "faint carries its line")
|
|
local dupes = 0
|
|
for _, e in ipairs(out) do
|
|
if e.kind == "message" and e.text and e.text:find("fainted") then dupes = dupes + 1 end
|
|
end
|
|
T.eq(dupes, 0, "the engine faint line is not repeated")
|
|
T.check(find(out, function(e) return e.kind == "choose-switch" end) ~= nil, "my replacement prompt opens the party")
|
|
T.eq(facade.awaiting, "replace", "awaiting a replacement")
|
|
refused = nil
|
|
h.forcedSwitch(s, 1)
|
|
T.eq(refused, "switch", "the fainted lead cannot be sent back")
|
|
T.eq(h.forcedSwitch(s, 2), true, "a legal replacement is accepted")
|
|
out = pumpBoth(me, bot, facade, 4)
|
|
local send = find(out, function(e) return e.kind == "send" and e.side == "player" end)
|
|
T.check(send and send.mon == facade.party[2] and send.text:find("Go!"), "the replacement sends out with Go!")
|
|
T.check(facade.player == facade.party[2], "battle.player follows the replacement")
|
|
end
|
|
|
|
do
|
|
local function fakeBs(legal)
|
|
return { seat = 0, names = { [0] = "GOLD", [1] = "MAY" }, seed = 1,
|
|
legal = function() return legal end, choose = function() return true end }
|
|
end
|
|
local recs = { F.record(t2, 25, { 33, 85, 1 }), F.record(t2, 1, { 33 }) }
|
|
local bs = fakeBs({ { kind = "move", slot = 2, locked = true }, { kind = "forfeit" } })
|
|
local facade = Gen2Facade.new(game, bs, {})
|
|
facade:translate({ { kind = "ready", parties = { [0] = recs, [1] = { recs[2] } } },
|
|
{ kind = "sendout", side = 0, index = 1, reason = "start" }, { kind = "sendout", side = 1, index = 1, reason = "start" },
|
|
{ kind = "prompt", what = "move", turn = 1 } })
|
|
T.eq(facade:lockedInMove(facade.player), "MOVE085", "a locked action names its move for auto-submit")
|
|
local act = facade:actionFor({ kind = "move", move = "MOVE033" })
|
|
T.check(act and act.slot == 2, "any submit maps to the locked action")
|
|
T.eq(facade:switchLocked(), true, "no switch actions means switching is locked")
|
|
|
|
bs = fakeBs({ { kind = "move", slot = 0 }, { kind = "forfeit" } })
|
|
facade = Gen2Facade.new(game, bs, {})
|
|
facade:translate({ { kind = "ready", parties = { [0] = recs, [1] = { recs[2] } } },
|
|
{ kind = "prompt", what = "move", turn = 1 } })
|
|
T.eq(facade:hasUsableMoves(facade.player), false, "only Struggle left")
|
|
act = facade:actionFor({ kind = "move", move = Battle.STRUGGLE })
|
|
T.check(act and act.slot == 0, "STRUGGLE submits slot 0")
|
|
|
|
local ev = facade:translate({
|
|
{ kind = "msg", id = "STRINGID_USEDMOVE", fill = { atk = { side = 1, index = 1 }, currentMove = { move = 33 } } },
|
|
{ kind = "msg", id = "STRINGID_ATTACKMISSED", fill = { atk = { side = 1, index = 1 } } },
|
|
{ kind = "hp", side = 0, from = facade.player.hp, to = facade.player.hp - 5, max = facade.player.maxHp },
|
|
{ kind = "hp", side = 0, from = facade.player.hp - 5, to = facade.player.hp - 2, max = facade.player.maxHp },
|
|
{ kind = "status", side = 0, status = "PSN" },
|
|
{ kind = "anim", anim = "status", name = "POISON", user = 0, target = 0 },
|
|
{ kind = "status", side = 1, status = "NONE" },
|
|
{ kind = "weather", weather = "RAINY", turns = 5 },
|
|
{ kind = "msg", id = "STRINGID_STARTEDTORAIN", fill = {} },
|
|
{ kind = "msg", id = "STRINGID_DEFENDERSSTATFELL", fill = { def = { side = 1, index = 1 }, stat = "spAtk", delta = -2 } },
|
|
{ kind = "msg", id = "STRINGID_NOT_A_REAL_ID", fill = { atk = { side = 7 } } },
|
|
{ kind = "msg", id = "STRINGID_PKMNHURTBY", fill = { atk = "nonsense" } },
|
|
{ kind = "withdraw", side = 1, index = 1, reason = "switch" },
|
|
{ kind = "sendout", side = 1, index = 1, reason = "switch" },
|
|
{ kind = "faint", side = 1 },
|
|
{ kind = "msg", id = "STRINGID_TARGETFAINTED", fill = { def = { side = 1, index = 1 } } },
|
|
{ kind = "need_replacement", side = 1, reason = "faint" },
|
|
{ kind = "end", result = { winner = 0, why = "faint" } },
|
|
})
|
|
local miss = ev[1]
|
|
T.check(miss.kind == "move" and miss.missed and miss.text:find("MAY") == nil and miss.text:find("MON001"),
|
|
"USEDMOVE with no move anim is a missed move line")
|
|
T.check(ev[2].kind == "message" and ev[2].text:find("missed"), "ATTACKMISSED text")
|
|
T.check(ev[3].kind == "damage" and ev[3].anim == false and ev[3].amount == 5, "non-hit damage has no shake")
|
|
T.check(ev[4].kind == "heal" and ev[4].amount == 3, "hp up is a heal")
|
|
T.check(ev[5].kind == "status" and ev[5].status == "poison" and ev[5].side == "player", "PSN -> poison")
|
|
T.check(ev[6].kind == "damage" and ev[6].anim == "ANIM_PSN" and ev[6].amount == 0 and ev[6].animSide == "player",
|
|
"status anim -> Gen 2 ANIM_PSN on the afflicted side")
|
|
T.check(ev[7].kind == "status" and ev[7].status == false, "NONE clears the status")
|
|
T.eq(facade.weather, "rain", "weather event sets the Gen 2 weather")
|
|
T.eq(ev[8].text, Strings(Effects.WEATHER_START_TEXT.rain), "weather line is the Gen 2 game's own")
|
|
T.check(ev[9].text:find("SPCL.ATK") and ev[9].text:find("sharply fell"), "stat text uses Gen 2 stat names")
|
|
T.check(ev[10].kind == "message" and ev[10].text:find("?", 1, true), "unknown ids drop, broken fills print ?")
|
|
T.check(ev[11].kind == "message" and ev[11].text:find("withdrew"), "enemy withdraw line")
|
|
T.check(ev[12].kind == "send" and ev[12].side == "enemy" and ev[12].text:find("sent out"), "enemy sendout line")
|
|
T.check(ev[13].kind == "faint" and ev[13].side == "enemy", "faint event")
|
|
T.check(ev[14].kind == "message" and ev[14].text:find("defeated"), "win line")
|
|
T.check(ev[15].kind == "trainer-return", "the trainer slides back in on a win")
|
|
T.eq(#ev, 15, "no extra events")
|
|
T.check(facade.over and facade.outcome == "win", "end sets over and outcome")
|
|
|
|
facade = Gen2Facade.new(game, bs, {})
|
|
facade:translate({ { kind = "ready", parties = { [0] = recs, [1] = { recs[2] } } } })
|
|
local d = facade:translate({ { kind = "over", outcome = "draw", why = "desync", detail = 3 } })
|
|
T.check(d[1] and d[1].text == Strings(Gen2Facade.TEXT.desync), "desync shows an original line")
|
|
T.check(facade.over and facade.outcome == "draw", "desync ends as a draw")
|
|
facade = Gen2Facade.new(game, bs, { names = { foe = "MAY" } })
|
|
facade:translate({ { kind = "ready", parties = { [0] = recs, [1] = { recs[2] } } } })
|
|
d = facade:translate({ { kind = "over", outcome = "draw", why = "disconnect" } })
|
|
T.check(d[1] and d[1].text:find("MAY"), "disconnect names the peer")
|
|
local b = Gen2Facade.banner()
|
|
T.check(b[1].text == "UNION RULES!" and b[2].text:find("GEN 3 battle rules") and b[2].text:find("held items"),
|
|
"union rules banner wording")
|
|
end
|
|
|
|
T.finish("g3u gen2 facade")
|