Port the Smart AI effect handlers (#28)
Translate every AI_Smart_EffectHandlers entry, so trainers with the Smart layer are no longer refused. The AI's type checks keep the CheckTypeMatchup bug: they read the move type and Foresight flag through hBattleTurn, and Conversion2's write to the player's move type persists. The reference emulator runs the unmodified AI_Smart routine over 6,024 states per browser, comparing scores, Random consumption, hBattleTurn and the player's move type. Validated with 579 unit tests, 151 browser tests, native/reference builds and reference/vendor/release checks. Kurt's balls, Park and Safari balls and special battle types remain on #28. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32d736ecae
commit
77704fe848
11 files changed
+682
-71
No files matched your search
+49
-4
@@ -25,7 +25,10 @@ const side = (): TrainerAISide => ({
|
||||
types: [0, 0],
|
||||
species: 1,
|
||||
dvs: 0x8888,
|
||||
stats: [20, 20, 20, 20, 20],
|
||||
level: 20,
|
||||
},
|
||||
levels: [7, 7, 7, 7, 7, 7, 7],
|
||||
sub: [0, 0, 0, 0, 0, 0],
|
||||
screens: 0,
|
||||
turnsTaken: 0,
|
||||
@@ -98,14 +101,14 @@ describe("trainer AI native scoring", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
it("refuses untranslated Smart effects before any earlier layer consumes randomness", () => {
|
||||
it("scores exhausted Smart moves along with the other layers", () => {
|
||||
const input = fixture(
|
||||
[move(1, C.EFFECT_ATTACK_DOWN, 0), move(2, C.EFFECT_REFLECT, 0)],
|
||||
[],
|
||||
[0, 0, 0, 0],
|
||||
);
|
||||
input.weights = 0x3d3;
|
||||
input.enemy.mon.pp[1] = 0; // Exhaustion does not bypass scoring or validation.
|
||||
expect(() => chooseTrainerMove(input)).toThrow(/Smart AI for move effect/);
|
||||
expect(chooseTrainerMove(input).index).toBe(0);
|
||||
});
|
||||
it("validates Smart effects across a supplied full trainer party", () => {
|
||||
expect(() =>
|
||||
@@ -117,7 +120,7 @@ describe("trainer AI native scoring", () => {
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
validateTrainerAI(0x3d3, [C.EFFECT_NORMAL_HIT, C.EFFECT_SLEEP]),
|
||||
).toThrow(/Smart AI/);
|
||||
).not.toThrow();
|
||||
});
|
||||
it("uses masked random slots and retries blanks without first-slot tie bias", () => {
|
||||
const input = fixture(undefined, [3, 6, 253]);
|
||||
@@ -338,4 +341,46 @@ describe("trainer AI native scoring", () => {
|
||||
})();
|
||||
expect(chooseTrainerMove(input).scores.slice(0, 2)).toEqual([15, 20]);
|
||||
});
|
||||
it("Smart Encore reads the move struct of hBattleTurn's side, not the player's last move", () => {
|
||||
const input = fixture(
|
||||
[move(1, C.EFFECT_ENCORE, 0), move(2), move(3, C.EFFECT_NORMAL_HIT, 40, 7)],
|
||||
[100, 0],
|
||||
);
|
||||
input.weights = 16;
|
||||
input.turn = 0;
|
||||
input.enemy.mon.stats = [20, 20, 30, 20, 20];
|
||||
input.player.lastMove = 3;
|
||||
input.player.move = { type: 9 };
|
||||
// Type 7 (the last move) would be neutral and discourage Encore; the
|
||||
// player's struct type 9 is immune, so Encore is encouraged instead.
|
||||
input.battleData.typeMatchups = () => [{ attacker: 9, defender: 0, multiplier: 0 }];
|
||||
expect(chooseTrainerMove(input)).toEqual({
|
||||
move: 1,
|
||||
index: 0,
|
||||
scores: [18, 20, 80, 80],
|
||||
enemyTurn: false,
|
||||
});
|
||||
});
|
||||
it("Smart Conversion2 writes the player's move struct type and leaves the player's turn", () => {
|
||||
const input = fixture([move(1, C.EFFECT_CONVERSION2, 0), move(2)], [200, 0]);
|
||||
input.weights = 16;
|
||||
input.conversion2Type = 5;
|
||||
input.player.move = { type: 9 };
|
||||
input.enemy.sub[1] = 1 << C.SUBSTATUS_IDENTIFIED;
|
||||
// Foresight on the enemy (the side opposite the player's turn) stops
|
||||
// before the Ghost rows, so only the first row applies.
|
||||
input.battleData.typeMatchups = () => [
|
||||
{ attacker: 5, defender: 0, multiplier: 20 },
|
||||
{ attacker: 0xfe, defender: 0xfe, multiplier: 0 },
|
||||
{ attacker: 5, defender: 0, multiplier: 0 },
|
||||
];
|
||||
expect(chooseTrainerMove(input)).toEqual({
|
||||
move: 1,
|
||||
index: 0,
|
||||
scores: [19, 20, 80, 80],
|
||||
enemyTurn: false,
|
||||
playerTurn: true,
|
||||
});
|
||||
expect(input.player.move.type).toBe(5);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user