Files
gen1recomp/tests/game3_scenario_battle_ai_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

163 lines
6.8 KiB
Lua

#!/usr/bin/env luajit
-- src/battle_ai_script_commands.c:299, src/battle_ai_script_commands.c:302, src/battle_ai_script_commands.c:310, src/battle_ai_script_commands.c:363, src/battle_ai_script_commands.c:371, src/battle_ai_script_commands.c:384
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
local State = require("src.core.game3.battle.state")
local Ai = require("src.core.game3.battle.ai")
local AiVm = require("src.core.game3.battle.ai_vm")
if not require("tests.game3_cache").mount() then
local Moves = require("src.core.game3.battle.moves")
Moves._romLoaded = true
-- src/data/battle_moves.h:432, :1160
Moves._rom = {
[33] = { effect = 0, power = 35, type = 0, accuracy = 95, pp = 35, secondaryChance = 0, target = 0, priority = 0, flags = 51 },
[89] = { effect = 147, power = 100, type = 4, accuracy = 100, pp = 10, secondaryChance = 0, target = 32, priority = 0, flags = 50 },
}
Moves.BY_NUM[33], Moves.BY_NUM[89] = "TACKLE", "EARTHQUAKE"
Moves._numByName = { TACKLE = 33, EARTHQUAKE = 89 }
end
local function loRng(lo, hi)
if lo and hi then return lo end
return 0
end
local scripts = {
SYN_KO = {
{ op = "if_can_faint", target = "SYN_TAKE" },
{ op = "score", delta = -40 },
{ op = "end" },
},
SYN_TAKE = { { op = "score", delta = 40 }, { op = "end" } },
SYN_FLAT = { { op = "score", delta = -25 }, { op = "end" } },
SYN_FLEE = { { op = "flee" } },
}
local synPack = { table = { "SYN_KO" }, data = {}, scripts = scripts }
local fleePack = { table = { "SYN_FLEE" }, data = {}, scripts = scripts }
local function aiState(opts)
opts = opts or {}
local st = State.new({
wild = opts.wild and true or false,
playerParty = { { species = 16, level = 20, hp = 10, maxHp = 40, moves = { 33 }, pp = { 35 } } },
foeMon = {
species = 74, level = 20, hp = 80, maxHp = 80,
moves = { 89, 33 }, pp = { 10, 35 },
attack = 70, defense = 60, spAtk = 40, spDef = 50, speed = 30,
},
})
st.player.type1 = 2 st.player.type2 = nil
st.enemy.type1 = 4 st.enemy.type2 = 5
st.aiFlags = opts.aiFlags or 1
return st
end
local function vmNew(st, scores, slot, pack)
return AiVm.new({
pack = pack or synPack,
st = st, user = st.enemy, target = st.player,
userSide = st.enemySide, targetSide = st.playerSide,
scores = scores, simulatedRNG = { 100, 100, 100, 100 },
movesetIndex = slot, rng = loRng,
})
end
print("[test] 1. Synthetic pack drives the AI script VM (always runs, no ROM)")
local s = { 100, 100, 100, 100 }
AiVm.run(vmNew(aiState(), s, 2), "SYN_FLAT")
check(s[2] == 75, "score op applies its delta as scripted (100 - 25 = 75)")
local s1 = { 100, 100, 100, 100 }
AiVm.run(vmNew(aiState(), s1, 1), "SYN_KO")
check(s1[1] == 60, "if_can_faint misses (EQ is Ground-immune here) -> 100 - 40 = 60, got " .. s1[1])
local s2 = { 100, 100, 100, 100 }
AiVm.run(vmNew(aiState(), s2, 2), "SYN_KO")
check(s2[2] == 127, "if_can_faint hits (Tackle can KO) -> 140 clamped to s8 max 127, got " .. s2[2])
local s3 = { 100, 100, 100, 100 }
AiVm.run(vmNew(aiState(), s3, 3), "SYN_KO")
check(s3[3] == 0, "empty moveset slot is zeroed by AiVm.run")
local vmF = vmNew(aiState(), { 100, 100, 100, 100 }, 1)
AiVm.run(vmF, "SYN_FLEE")
check(vmF.aiAction == 0xB, string.format("flee op sets AI_ACTION_FLEE|DONE|DO_NOT_ATTACK (aiAction=0x%X)", vmF.aiAction))
print("[test] 2. Ai.chooseMove picks the fainting move on the rigged state")
local act = Ai.chooseMove(aiState(), { pack = synPack, aiFlags = 1, rng = loRng })
check(act ~= nil and act.kind == "move", "returns a move action")
check(act.slot == 2 and act.move == 33, "picks Tackle over the Ground-immune EQ (slot=" .. tostring(act.slot) .. ")")
check(act.scores[1] == 60 and act.scores[2] == 127, "decision carries the scripted scores 60 / 127")
check(act.scores[1] < act.scores[2], "lower-scoring move was not chosen")
check(act.target == nil, "singles: target trimmed from the returned action")
print("[test] 3. aiFlags gating: 0 = no scripts, nonzero = bit loop runs pack")
local a0 = Ai.chooseMove(aiState(), { pack = synPack, aiFlags = 0, rng = loRng })
check(a0 and a0.kind == "move", "flags=0 still returns a usable move")
check(a0.scores[1] == 100 and a0.scores[2] == 100, "flags=0: base scores 100 untouched (script never ran)")
check(a0.slot == 1, "flags=0: 100/100 tie broken by rigged rng -> first slot")
local a1 = Ai.chooseMove(aiState(), { pack = synPack, aiFlags = 1, rng = loRng })
check(a1.scores[1] == 60 and a1.slot == 2, "flags=1: logic slot 0 (SYN_KO) ran and changed the pick")
local a7 = Ai.chooseMove(aiState(), { pack = synPack, aiFlags = 7, rng = loRng })
check(a7.scores[1] == a1.scores[1] and a7.scores[2] == a1.scores[2] and a7.slot == a1.slot,
"flags=7: empty table slots 1..2 skipped safely, same decision as flags=1")
local wildRnd = Ai.chooseMove(aiState({ wild = true, aiFlags = 0 }), { rng = loRng })
check(wildRnd and wildRnd.kind == "move" and wildRnd.scores[1] == 0,
"wild + flags=0 bypasses AI entirely (random pick, scores all 0)")
print("[test] 4. Ai.chooseAction delivers the enemy's round action (wild battle)")
local stA = aiState({ wild = true })
local okBS = pcall(Ai.battleStart, stA, { rng = loRng })
check(okBS and type(stA._aiHistory) == "table", "Ai.battleStart initialises the item-use history")
local actA = Ai.chooseAction(stA, 1, { pack = synPack, aiFlags = 1, rng = loRng })
check(actA and actA.kind == "move" and actA.slot == 2,
"wild battle: chooseAction returns the scored move (Tackle, slot 2)")
local actF = Ai.chooseAction(aiState({ wild = true }), 1, { pack = fleePack, aiFlags = 1, rng = loRng })
check(actF and actF.kind == "run", "AI_ACTION_FLEE surfaces as a kind='run' action")
print("[test] 5. Real extracted AI pack (self-skips when absent)")
local okPack, realPack = pcall(Ai.loadPack, { force = true, extract = true })
if not okPack or not realPack then
print("[skip] real AI pack absent")
else
-- data/battle_ai_scripts.s:17, :52, :2767
check(type(realPack.table) == "table" and #realPack.table == 32, "gBattleAI_ScriptsTable has 32 entries")
local badMove = realPack.scripts[realPack.table[1]]
check(badMove and badMove[1].op == "get_how_powerful_move_is", "table[0] = AI_CheckBadMove")
local faint = realPack.scripts[realPack.table[3]]
check(faint and faint[1].op == "if_can_faint", "table[2] = AI_TryToFaint")
for name, body in pairs(realPack.scripts) do
for _, op in ipairs(body) do
if op.target and not realPack.scripts[op.target] then
check(false, "branch target " .. op.target .. " from " .. name .. " has a body")
end
end
end
end
finish()