mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
123feef3a1
CLOSES #2425, CLOSES #2428, CLOSES #2431, CLOSES #2432, CLOSES #2433, CLOSES #2435, CLOSES #2436, CLOSES #2437, CLOSES #2440
542 lines
22 KiB
Lua
542 lines
22 KiB
Lua
#!/usr/bin/env luajit
|
|
-- FireRed battle AI pack + scoring VM smoke tests.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
require("tests.game3_cache").requireData("game3_battle_ai_test")
|
|
|
|
local failed = 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
print("[test] 1. Load / extract AI pack")
|
|
local Ai = require("src.core.game3.battle.ai")
|
|
local pack = Ai.loadPack({ force = true, extract = true })
|
|
check(pack ~= nil, "pack loaded")
|
|
-- data/battle_ai_scripts.s:17, :52, :624, :2767, :3258
|
|
local function scoreScript(p, delta)
|
|
for name, body in pairs(p.scripts) do
|
|
if #body == 2 and body[1].op == "score" and body[1].delta == delta and body[2].op == "end" then return name end
|
|
end
|
|
end
|
|
local SCORE_MINUS10 = pack and scoreScript(pack, -10)
|
|
check(pack and #pack.table == 32, "gBattleAI_ScriptsTable has 32 entries")
|
|
local badMove = pack and pack.scripts[pack.table[1]]
|
|
check(badMove and badMove[1].op == "get_how_powerful_move_is" and badMove[2].op == "if_equal"
|
|
and badMove[2].value == 0, "table[0] = AI_CheckBadMove (get_how_powerful_move_is, if_equal MOVE_POWER_DISCOURAGED)")
|
|
local faint = pack and pack.scripts[pack.table[3]]
|
|
check(faint and faint[1].op == "if_can_faint" and faint[2].op == "get_how_powerful_move_is",
|
|
"table[2] = AI_TryToFaint (if_can_faint, get_how_powerful_move_is)")
|
|
local ret = pack and pack.scripts[pack.table[11]]
|
|
check(ret and #ret == 1 and ret[1].op == "end" and pack.table[11] == pack.table[29], "table[10..28] = AI_Ret")
|
|
check(SCORE_MINUS10 ~= nil, "Score_Minus10 (score -10, end) present")
|
|
|
|
print("[test] 2. Brock aiFlags == 7")
|
|
local trainersRoot = require("tests.game3_cache").root("trainers.lua")
|
|
local trainersPath = trainersRoot and (trainersRoot .. "/trainers.lua")
|
|
local tf = trainersPath and io.open(trainersPath, "rb")
|
|
if tf then
|
|
tf:close()
|
|
local t = assert(loadfile(trainersPath))()
|
|
local brock = t.trainers and t.trainers[414]
|
|
check(brock ~= nil, "BROCK trainer 414 exists")
|
|
check(brock and brock.aiFlags == 7, "BROCK aiFlags == 7")
|
|
else
|
|
local Trainers = require("src.core.game3.scripting.trainers")
|
|
-- fallback: hard expectation from extract contract
|
|
check(true, "trainers.lua missing in cache; skip live BROCK row")
|
|
end
|
|
|
|
print("[test] 3. Score starts 100; score -10 → 90")
|
|
local AiVm = require("src.core.game3.battle.ai_vm")
|
|
local State = require("src.core.game3.battle.state")
|
|
local st0 = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 16, level = 10, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 } } }, -- Pidgey
|
|
foeMon = { species = 74, level = 10, hp = 30, maxHp = 30, moves = { 89 }, pp = { 10 } }, -- Geodude / EQ
|
|
})
|
|
st0.aiFlags = 0
|
|
local scores = { 100, 100, 100, 100 }
|
|
local vm = AiVm.new({
|
|
pack = pack,
|
|
st = st0,
|
|
user = st0.enemy,
|
|
target = st0.player,
|
|
userSide = st0.enemySide,
|
|
targetSide = st0.playerSide,
|
|
scores = scores,
|
|
simulatedRNG = { 100, 100, 100, 100 },
|
|
movesetIndex = 1,
|
|
rng = function() return 0 end,
|
|
})
|
|
AiVm.run(vm, SCORE_MINUS10)
|
|
check(scores[1] == 90, "score 100 + (-10) = 90 (got " .. tostring(scores[1]) .. ")")
|
|
|
|
print("[test] 4. CheckBadMove: Ground vs Flying scored much lower than neutral")
|
|
-- Enemy has EARTHQUAKE (89, Ground) and TACKLE (33, Normal) vs Flying target
|
|
local stBad = State.new({
|
|
wild = false,
|
|
playerParty = { {
|
|
species = 16, level = 20, hp = 50, maxHp = 50,
|
|
moves = { 33 }, pp = { 35 },
|
|
} },
|
|
foeMon = {
|
|
species = 74, level = 20, hp = 50, maxHp = 50,
|
|
moves = { 89, 33 }, pp = { 10, 35 },
|
|
type1 = 4, -- force later via battler
|
|
},
|
|
})
|
|
-- Ensure types: player Flying
|
|
stBad.player.type1 = 2 -- FLYING
|
|
stBad.player.type2 = nil
|
|
stBad.enemy.type1 = 4 -- GROUND
|
|
stBad.enemy.type2 = 5 -- ROCK
|
|
stBad.aiFlags = 1 -- AI_SCRIPT_CHECK_BAD_MOVE only
|
|
|
|
local actBad = Ai.chooseMove(stBad, {
|
|
pack = pack,
|
|
aiFlags = 1,
|
|
rng = function(a, b)
|
|
if a and b then return a end
|
|
return 0
|
|
end,
|
|
})
|
|
check(actBad and actBad.scores ~= nil, "CheckBadMove returned scores")
|
|
local sEq = actBad.scores[1]
|
|
local sTk = actBad.scores[2]
|
|
check(sEq ~= nil and sTk ~= nil, "both move scores present")
|
|
check(sEq < sTk - 5, string.format("EQ vs Flying (%s) << Tackle (%s)", tostring(sEq), tostring(sTk)))
|
|
check(actBad.move == 33 or actBad.slot == 2, "prefers Tackle over Earthquake")
|
|
|
|
print("[test] 5. TryToFaint / viability: KO move preferred when flags=7")
|
|
local stKo = State.new({
|
|
wild = false,
|
|
playerParty = { {
|
|
species = 19, level = 5, hp = 5, maxHp = 40,
|
|
moves = { 33 }, pp = { 35 },
|
|
} },
|
|
foeMon = {
|
|
species = 4, level = 20, hp = 60, maxHp = 60,
|
|
moves = { 52, 45 }, -- Ember, Growl
|
|
pp = { 25, 40 },
|
|
},
|
|
})
|
|
stKo.player.type1 = 0
|
|
stKo.enemy.type1 = 10 -- FIRE
|
|
local actKo = Ai.chooseMove(stKo, {
|
|
pack = pack,
|
|
aiFlags = 7,
|
|
rng = function(a, b)
|
|
if a and b then return a end
|
|
return 0
|
|
end,
|
|
})
|
|
check(actKo and actKo.kind == "move", "flags=7 returns a move")
|
|
check(actKo.slot == 1 or actKo.move == 52 or actKo.move == "EMBER",
|
|
"prefer damaging Ember over Growl (slot=" .. tostring(actKo.slot) .. " move=" .. tostring(actKo.move) .. ")")
|
|
check(actKo.scores and actKo.scores[1] > actKo.scores[2],
|
|
string.format("Ember score (%s) > Growl (%s)", tostring(actKo.scores and actKo.scores[1]), tostring(actKo.scores and actKo.scores[2])))
|
|
|
|
print("[test] 6. aiFlags=0 / wild → usable move")
|
|
local stWild = State.new({
|
|
wild = true,
|
|
playerParty = { { species = 1, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = { species = 16, level = 3, hp = 15, maxHp = 15, moves = { 16, 33 }, pp = { 35, 35 } },
|
|
})
|
|
stWild.aiFlags = 0
|
|
local actW = Ai.chooseMove(stWild, { pack = pack, aiFlags = 0 })
|
|
check(actW and actW.kind == "move", "wild returns move")
|
|
check(actW.move ~= nil and actW.move ~= 0, "wild move usable")
|
|
|
|
print("[test] 7. Commands.enemyAction uses AI")
|
|
local Commands = require("src.core.game3.battle.commands")
|
|
stKo.aiFlags = 7
|
|
local ea = Commands.enemyAction(stKo)
|
|
check(ea and ea.kind == "move", "enemyAction returns move")
|
|
check(ea.user == "enemy", "enemyAction user=enemy")
|
|
|
|
print("[test] 8. Doubles: flank target, absent flip, BOTH / USER targets, partner-aware scans")
|
|
local AiCmds = require("src.core.game3.battle.ai_cmds")
|
|
local function dstate(foeMoves)
|
|
local pp = { 20, 20, 20, 20 }
|
|
local st = State.new({
|
|
wild = false,
|
|
double = true,
|
|
playerParty = {
|
|
{ species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } },
|
|
{ species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } },
|
|
},
|
|
foeParty = {
|
|
{ species = 74, level = 20, hp = 50, maxHp = 50, moves = foeMoves, pp = pp },
|
|
{ species = 74, level = 20, hp = 50, maxHp = 50, moves = foeMoves, pp = pp },
|
|
{ species = 4, level = 20, hp = 50, maxHp = 50, moves = { 52 }, pp = { 25 } },
|
|
},
|
|
})
|
|
st.aiFlags = 1
|
|
return st
|
|
end
|
|
local function flankRng(flank)
|
|
return function(lo, hi)
|
|
if lo == 0 and hi == 65535 then return flank end
|
|
if lo and hi then return lo end
|
|
return 0
|
|
end
|
|
end
|
|
|
|
local stD = dstate({ 33 })
|
|
local aD = Ai.chooseMove(stD, { battler = 1, pack = pack, aiFlags = 1, rng = flankRng(2) })
|
|
check(aD and aD.battler == 1 and aD.target == 2, "doubles: Random() & BIT_FLANK picks player right (target=" .. tostring(aD and aD.target) .. ")")
|
|
stD.absent[2] = true
|
|
aD = Ai.chooseMove(stD, { battler = 1, pack = pack, aiFlags = 1, rng = flankRng(2) })
|
|
check(aD and aD.target == 0, "doubles: absent flank flips to player left")
|
|
|
|
local stG = dstate({ 45 })
|
|
local aG = Ai.chooseMove(stG, { battler = 1, pack = pack, aiFlags = 1, rng = flankRng(2) })
|
|
check(aG and aG.target == 0, "doubles: BOTH move targets player left")
|
|
stG.absent[0] = true
|
|
aG = Ai.chooseMove(stG, { battler = 1, pack = pack, aiFlags = 1, rng = flankRng(2) })
|
|
check(aG and aG.target == 2, "doubles: BOTH move falls back to player right when left absent")
|
|
|
|
local stH = dstate({ 270, 33 })
|
|
local aH = Ai.chooseMove(stH, { battler = 3, pack = pack, aiFlags = 1, rng = flankRng(0) })
|
|
check(aH and aH.slot == 1 and aH.target == 3, "doubles: Helping Hand (USER) targets self")
|
|
local stHs = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = { species = 74, level = 20, hp = 50, maxHp = 50, moves = { 270, 33 }, pp = { 20, 20 } },
|
|
})
|
|
local aHs = Ai.chooseMove(stHs, { pack = pack, aiFlags = 1, rng = flankRng(0) })
|
|
check(aH.scores and aHs.scores and aH.scores[1] > aHs.scores[1],
|
|
string.format("Helping Hand not penalized in doubles (%s) vs singles (%s)",
|
|
tostring(aH.scores and aH.scores[1]), tostring(aHs.scores and aHs.scores[1])))
|
|
|
|
local vmD = AiVm.new({
|
|
pack = pack, st = stH, user = stH.battlers[3], target = stH.battlers[0],
|
|
userSide = stH.enemySide, targetSide = stH.playerSide,
|
|
scores = { 100, 100, 100, 100 }, simulatedRNG = { 100, 100, 100, 100 },
|
|
movesetIndex = 1, rng = flankRng(0),
|
|
})
|
|
AiCmds.dispatch(vmD, { op = "is_double_battle" })
|
|
check(vmD.funcResult == 1, "is_double_battle = 1 in doubles")
|
|
AiCmds.dispatch(vmD, { op = "count_alive_pokemon", battler = 1 })
|
|
check(vmD.funcResult == 1, "count_alive_pokemon excludes both on-field foes (got " .. tostring(vmD.funcResult) .. ")")
|
|
|
|
local aA = Ai.chooseAction(dstate({ 33 }), 3, { pack = pack, aiFlags = 1, rng = flankRng(0) })
|
|
check(aA and aA.kind == "move" and aA.battler == 3 and (aA.target == 0 or aA.target == 2),
|
|
"chooseAction returns {kind, battler, move, slot, target}")
|
|
local ea3 = Commands.enemyAction((function() local s = dstate({ 33 }); s.rng = flankRng(2); return s end)(), 3)
|
|
check(ea3 and ea3.battler == 3 and ea3.target == 2, "Commands.enemyAction(st, 3) routes through Ai.chooseAction")
|
|
|
|
print("[test] 9. Singles pret paths: tie-break quirk, wild random move, zero-flag trainer setup")
|
|
local function recRng(fixed)
|
|
local calls = {}
|
|
local f = function(lo, hi)
|
|
calls[#calls + 1] = { lo, hi }
|
|
if fixed and fixed[lo .. ":" .. hi] ~= nil then return fixed[lo .. ":" .. hi] end
|
|
if lo and hi then return lo end
|
|
return 0
|
|
end
|
|
return f, calls
|
|
end
|
|
local stQ = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = { species = 74, level = 20, hp = 50, maxHp = 50, moves = { 33, 33 }, pp = { 0, 20 } },
|
|
})
|
|
local qRng, qCalls = recRng()
|
|
local aQ = Ai.chooseMove(stQ, { pack = pack, aiFlags = 0, rng = qRng })
|
|
local lastPick = qCalls[#qCalls]
|
|
check(aQ and aQ.slot == 2, "zero-flag trainer picks the only usable move")
|
|
check(lastPick and lastPick[1] == 1 and lastPick[2] == 2, "new best counted twice (Random() % 2)")
|
|
local draws = 0
|
|
for _, c in ipairs(qCalls) do if c[1] == 0 and c[2] == 15 then draws = draws + 1 end end
|
|
check(draws == 4, "zero-flag trainer still rolls 4 simulatedRNG draws")
|
|
|
|
local stW = State.new({
|
|
wild = true,
|
|
playerParty = { { species = 1, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = { species = 16, level = 3, hp = 15, maxHp = 15, moves = { 16, 33 }, pp = { 35, 35 } },
|
|
})
|
|
local wRng, wCalls = recRng({ ["0:3"] = 1 })
|
|
local aW = Ai.chooseMove(stW, { pack = pack, rng = wRng })
|
|
check(aW and aW.slot == 2, "wild: Random() & 3 picks slot 2")
|
|
check(#wCalls == 1, "wild: one RNG draw, no AI setup")
|
|
|
|
local stE = State.new({
|
|
wild = false,
|
|
playerParty = {
|
|
{ species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } },
|
|
{ species = 16, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 }, isEgg = true },
|
|
{ species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } },
|
|
},
|
|
foeMon = { species = 74, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 20 } },
|
|
})
|
|
local vmE = AiVm.new({
|
|
pack = pack, st = stE, user = stE.enemy, target = stE.player,
|
|
userSide = stE.enemySide, targetSide = stE.playerSide,
|
|
scores = { 100, 100, 100, 100 }, simulatedRNG = { 100, 100, 100, 100 }, movesetIndex = 1,
|
|
rng = function(lo) return lo or 0 end,
|
|
})
|
|
AiCmds.dispatch(vmE, { op = "count_alive_pokemon", battler = 0 })
|
|
check(vmE.funcResult == 1, "count_alive_pokemon skips eggs in singles (got " .. tostring(vmE.funcResult) .. ")")
|
|
|
|
print("[test] 10. Trainer items: ShouldUseItem + enemy item execution")
|
|
local Engine = require("src.core.game3.battle.engine")
|
|
local Adapter = require("src.core.game3.battle.adapter")
|
|
local function itemState(opts)
|
|
local st = State.new({
|
|
wild = opts.wild or false,
|
|
playerParty = { { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = { species = 74, level = 20, hp = opts.hp or 5, maxHp = 40, moves = { 33 }, pp = { 20 },
|
|
status = opts.status },
|
|
})
|
|
st.trainerItems = opts.items
|
|
st.trainerClassName, st.trainerName = "LEADER", "BROCK"
|
|
st.aiFlags = 1
|
|
st.rng = function(lo, hi) if lo and hi then return lo end return 0 end
|
|
return st
|
|
end
|
|
local stP = itemState({ items = { 13, 0, 0, 0 } })
|
|
local aP = Ai.chooseAction(stP, 1, { pack = pack })
|
|
check(aP and aP.kind == "item" and aP.item == 13 and aP.battler == 1 and aP.target == 1,
|
|
"low HP trainer mon uses POTION (kind=" .. tostring(aP and aP.kind) .. ")")
|
|
check(stP._aiHistory.items[1] == 0, "POTION removed from battle history")
|
|
local aP2 = Ai.chooseAction(stP, 1, { pack = pack })
|
|
check(aP2 and aP2.kind == "move", "no second POTION")
|
|
local adP = Adapter.new(stP, function() end)
|
|
local msgs = {}
|
|
adP._say = function(t) msgs[#msgs + 1] = t end
|
|
Engine.performEnemyItem(stP, adP, aP)
|
|
check(tonumber(stP.enemy.mon.hp) == 25, "POTION heals 20 (hp=" .. tostring(stP.enemy.mon.hp) .. ")")
|
|
check(msgs[1] and msgs[1]:find("used POTION!", 1, true) ~= nil, "prints '<TRAINER> used POTION!'")
|
|
check(msgs[2] and msgs[2]:find("restored health!", 1, true) ~= nil, "prints 'restored health!'")
|
|
|
|
local stF = itemState({ items = { 23, 0, 0, 0 }, hp = 40, status = "PSN" })
|
|
local aF = Ai.chooseAction(stF, 1, { pack = pack })
|
|
check(aF and aF.kind == "item" and aF.item == 23 and aF.aiItemFlags == 0x10, "poisoned mon uses FULL HEAL")
|
|
Engine.performEnemyItem(stF, Adapter.new(stF, function() end), aF)
|
|
check(stF.enemy.mon.status == nil, "FULL HEAL cures poison")
|
|
|
|
local stWi = itemState({ items = { 13, 0, 0, 0 }, wild = true })
|
|
local aWi = Ai.chooseAction(stWi, 1, { pack = pack })
|
|
check(aWi and aWi.kind == "move", "wild mons never use items")
|
|
|
|
print("[test] 11. Singles ShouldSwitch: Perish Song count 0 switches out")
|
|
local stS = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } } },
|
|
foeParty = {
|
|
{ species = 74, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 20 } },
|
|
{ species = 4, level = 20, hp = 50, maxHp = 50, moves = { 52 }, pp = { 25 } },
|
|
},
|
|
foeMon = nil,
|
|
})
|
|
stS.aiFlags = 1
|
|
stS.rng = function(lo, hi) if lo and hi then return lo end return 0 end
|
|
stS.enemy.perishSong, stS.enemy.expPerishTurns = true, 0
|
|
local aS = Commands.enemyAction(stS)
|
|
check(aS and aS.kind == "switch" and aS.slot == 2 and aS.battler == 1, "Perish Song 0 -> switch to slot 2")
|
|
stS.wild = true
|
|
stS.monToSwitchInto = {}
|
|
local aS2 = Ai.chooseAction(stS, 1, { pack = pack })
|
|
check(aS2 and aS2.kind == "move", "wild mons never switch")
|
|
|
|
print("[test] 12. Complete 743 Trainer aiFlags ROM parity vs pokefirered/src/data/trainers.h")
|
|
do
|
|
local f = io.open("pokefirered/src/data/trainers.h", "r")
|
|
if f then
|
|
local okT, trainerPack = pcall(require, "data.generated.gba.trainers")
|
|
if not okT or not trainerPack then
|
|
f:close()
|
|
print("[skip] data.generated.gba.trainers not found")
|
|
return
|
|
end
|
|
local content = f:read("*a")
|
|
f:close()
|
|
local trainerBlocks = {}
|
|
local currentId = nil
|
|
local currentAiFlags = 0
|
|
for line in content:gmatch("[^\r\n]+") do
|
|
local tid = line:match("%[([%w_]+)%]%s*=%s*{")
|
|
if tid then
|
|
currentId = tid
|
|
currentAiFlags = 0
|
|
elseif line:match("%.aiFlags%s*=%s*(.-),") then
|
|
local expr = line:match("%.aiFlags%s*=%s*(.-),")
|
|
local val = 0
|
|
if expr:find("AI_SCRIPT_CHECK_BAD_MOVE") then val = val + 1 end
|
|
if expr:find("AI_SCRIPT_CHECK_VIABILITY") then val = val + 2 end
|
|
if expr:find("AI_SCRIPT_TRY_TO_FAINT") then val = val + 4 end
|
|
if expr:find("AI_SCRIPT_SETUP_FIRST_TURN") then val = val + 8 end
|
|
if expr:find("AI_SCRIPT_RISKY") then val = val + 16 end
|
|
if expr:find("AI_SCRIPT_PREFER_STRONGEST_MOVE") then val = val + 32 end
|
|
if expr:find("AI_SCRIPT_PREFER_BATON_PASS") then val = val + 64 end
|
|
if expr:find("AI_SCRIPT_DOUBLE_BATTLE") then val = val + 128 end
|
|
if expr:find("AI_SCRIPT_HP_AWARE") then val = val + 256 end
|
|
if expr:find("AI_SCRIPT_ROAMING") then val = val + 0x20000000 end
|
|
if expr:find("AI_SCRIPT_SAFARI") then val = val + 0x40000000 end
|
|
if expr:find("AI_SCRIPT_FIRST_BATTLE") then val = val + 0x80000000 end
|
|
currentAiFlags = val
|
|
trainerBlocks[#trainerBlocks + 1] = { id = currentId, aiFlags = currentAiFlags }
|
|
elseif currentId and line:match("^%s*},") then
|
|
if #trainerBlocks == 0 or trainerBlocks[#trainerBlocks].id ~= currentId then
|
|
trainerBlocks[#trainerBlocks + 1] = { id = currentId, aiFlags = 0 }
|
|
end
|
|
currentId = nil
|
|
end
|
|
end
|
|
check(#trainerBlocks == 743, "found 743 pret trainer definitions")
|
|
local mismatches = 0
|
|
for idx, pretT in ipairs(trainerBlocks) do
|
|
local tid = idx - 1
|
|
local romT = trainerPack.trainers[tid]
|
|
if not romT or romT.aiFlags ~= pretT.aiFlags then
|
|
mismatches = mismatches + 1
|
|
end
|
|
end
|
|
check(mismatches == 0, "all 743 ROM-extracted trainer aiFlags match pret (0 mismatches)")
|
|
else
|
|
check(true, "pokefirered headers missing; skip parity scan")
|
|
end
|
|
end
|
|
|
|
print("[test] 13. Scripted Wild Battles: dowildbattle sets wildScripted -> aiFlags = 1 (AI_SCRIPT_CHECK_BAD_MOVE)")
|
|
do
|
|
local stScripted = State.new({
|
|
wild = true,
|
|
playerParty = { { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = {
|
|
species = 74, level = 20, hp = 50, maxHp = 50,
|
|
moves = { 89, 33 }, pp = { 10, 35 }, -- EQ (Ground) and Tackle (Normal)
|
|
},
|
|
})
|
|
stScripted.player.type1 = 2 -- FLYING
|
|
stScripted.player.type2 = nil
|
|
stScripted.enemy.type1 = 4 -- GROUND
|
|
stScripted.enemy.type2 = 5 -- ROCK
|
|
stScripted.wildScripted = true
|
|
|
|
local actScripted = Ai.chooseMove(stScripted, {
|
|
pack = pack,
|
|
rng = function(a, b)
|
|
if a and b then return a end
|
|
return 0
|
|
end,
|
|
})
|
|
check(actScripted and actScripted.scores ~= nil, "scripted wild battle executed AI scripts")
|
|
check(actScripted.move == 33 or actScripted.slot == 2, "scripted wild avoids ineffective Earthquake vs Flying")
|
|
end
|
|
|
|
print("[test] 14. Legendary Wild Battles: StartLegendaryBattle sets legendary -> aiFlags = 7")
|
|
do
|
|
local stLegend = State.new({
|
|
wild = true,
|
|
playerParty = { { species = 19, level = 5, hp = 5, maxHp = 40, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = {
|
|
species = 146, level = 50, hp = 150, maxHp = 150, -- Moltres
|
|
moves = { 52, 45 }, -- Ember, Growl
|
|
pp = { 25, 40 },
|
|
},
|
|
})
|
|
stLegend.player.type1 = 0
|
|
stLegend.enemy.type1 = 10 -- FIRE
|
|
stLegend.legendary = true
|
|
|
|
local actLegend = Ai.chooseMove(stLegend, {
|
|
pack = pack,
|
|
rng = function(a, b)
|
|
if a and b then return a end
|
|
return 0
|
|
end,
|
|
})
|
|
check(actLegend and actLegend.scores ~= nil, "legendary battle executed AI scripts (flags=7)")
|
|
check(actLegend.slot == 1 or actLegend.move == 52, "legendary prefers KO Ember over Growl")
|
|
check(actLegend.scores[1] > actLegend.scores[2], "Ember score > Growl score under legendary AI")
|
|
end
|
|
|
|
print("[test] 15. 32-bit unsigned AI_SCRIPT_FIRST_BATTLE (0x80000000) safety")
|
|
do
|
|
local stFirst = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 1, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = { species = 4, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } },
|
|
})
|
|
stFirst.aiFlags = 0x80000000
|
|
local actFirst = Ai.chooseMove(stFirst, { pack = pack })
|
|
check(actFirst and actFirst.kind == "move", "aiFlags 0x80000000 evaluates safely without sign overflow")
|
|
|
|
-- pokefirered/src/battle_ai_script_commands.c:331
|
|
local stTut = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 19, level = 5, hp = 5, maxHp = 40, moves = { 33 }, pp = { 35 } } },
|
|
foeMon = {
|
|
species = 4, level = 5, hp = 20, maxHp = 20,
|
|
moves = { 52, 45 },
|
|
pp = { 25, 40 },
|
|
},
|
|
})
|
|
stTut.player.type1 = 0
|
|
stTut.enemy.type1 = 10
|
|
stTut.firstBattle = true
|
|
stTut.aiFlags = 7
|
|
local actTut = Ai.chooseMove(stTut, {
|
|
pack = pack,
|
|
rng = function(a, b)
|
|
if a and b then return a end
|
|
return 0
|
|
end,
|
|
})
|
|
check(actTut and actTut.scores ~= nil, "firstBattle trainer battle executed AI scripts")
|
|
check(actTut and actTut.scores and actTut.scores[1] > actTut.scores[2],
|
|
"firstBattle keeps the trainer's own aiFlags (7) instead of AI_SCRIPT_FIRST_BATTLE")
|
|
end
|
|
|
|
print("[test] 16. GetBattleOutcome (special 0xB4) and B_OUTCOME constants")
|
|
do
|
|
local Natives = require("src.core.game3.scripting.natives")
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
check(Natives.B_OUTCOME.WON == 1, "B_OUTCOME.WON == 1")
|
|
check(Natives.B_OUTCOME.LOST == 2, "B_OUTCOME.LOST == 2")
|
|
check(Natives.B_OUTCOME.RAN == 4, "B_OUTCOME.RAN == 4")
|
|
check(Natives.B_OUTCOME.CAUGHT == 7, "B_OUTCOME.CAUGHT == 7")
|
|
|
|
check(Natives.outcome_to_code("win") == 1, "win -> 1")
|
|
check(Natives.outcome_to_code("caught") == 7, "caught -> 7")
|
|
check(Natives.outcome_to_code("ran") == 4, "ran -> 4")
|
|
|
|
local ctx = { specialVars = {} }
|
|
ctx.lastBattleOutcome = Natives.B_OUTCOME.CAUGHT
|
|
Natives.special(ctx, 0xB4, nil)
|
|
check(Flags.getVar(nil, ctx, 0x800D) == 7, "GetBattleOutcome writes CAUGHT (7) to VAR_RESULT (0x800D)")
|
|
end
|
|
|
|
print("[test] 17. Oak's Tutorial Battle (oldManTutorial) skips AI item/switch")
|
|
do
|
|
local stTut = State.new({
|
|
wild = false,
|
|
playerParty = { { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 } } },
|
|
foeParty = {
|
|
{ species = 13, level = 5, hp = 5, maxHp = 20, moves = { 33 }, pp = { 20 } },
|
|
{ species = 13, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 20 } },
|
|
},
|
|
foeMon = nil,
|
|
})
|
|
stTut.oldManTutorial = true
|
|
stTut.enemy.perishSong, stTut.enemy.expPerishTurns = true, 0
|
|
stTut.enemyItems = { 13, 0, 0, 0 }
|
|
local actTut = Ai.chooseAction(stTut, 1, { pack = pack })
|
|
check(actTut and actTut.kind == "move", "oldManTutorial skips tactical switches and item use")
|
|
end
|
|
|
|
if failed > 0 then
|
|
print(string.format("\n%d FAILED", failed))
|
|
os.exit(1)
|
|
end
|
|
print("\nAll game3 battle AI tests passed.")
|
|
|