From 79e1ae2f6ab52e277dca951ca3e2eadd2f013d33 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 4 Jun 2026 00:16:48 -0400 Subject: [PATCH] Decompile Pokedex, Pokemon Recall, Imposter Oak's Revenge, Digger Finished the last whole-card AI functions straight from the disassembly (make compare proves byte identity); every AITrainerCardLogic row now resolves to named functions. - AIPlay/AIDecide_PokemonRecall ($7a90/$7aa5): decks $12 Overflow and $18 Psychokinesis rescue a discarded evolution whose pre-evo is in play/hand. - AIPlay/AIDecide_ImposterOaksRevenge ($7ec3/$7ed4): decks $48 Eevee Showdown and $6b Ronald's Power play it to disrupt a 5+ card opponent hand. - AIPlay/AIDecide_Digger ($7f30/$7f3c): always plays when able. - AIPlay/AIDecide_Pokedex ($5e94/$5ebd): occasional top-5 reorder (Pokemon, then Trainers, then Energy) + two type-specific energy helpers and GetCardTypeFromDeckIndex that shared the gap. (Disassembler had labeled card $150 as the boot symbol Start since both are $0150; it is really SPEAROW_LV13 -- caught by make compare.) Bank 8: 95.71% -> 99.29%. make compare OK. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/poketcg2-ai/SKILL.md | 2 +- src/engine/bank08.asm | 378 +++++++++++++++++++++++++++- 2 files changed, 375 insertions(+), 5 deletions(-) diff --git a/.claude/skills/poketcg2-ai/SKILL.md b/.claude/skills/poketcg2-ai/SKILL.md index 9fe405f..20c69d3 100644 --- a/.claude/skills/poketcg2-ai/SKILL.md +++ b/.claude/skills/poketcg2-ai/SKILL.md @@ -337,7 +337,7 @@ the target (helps justify future decomp prioritization). ## Bank $08 decompilation status -**Source-defined**: 95.71% (~15.3 KiB of 16 KiB). Whole-card deciders done except remaining raw: Pokedex, Pokemon Recall, Imposter Oak Revenge, Digger. +**Source-defined**: 99.29% (~15.9 KiB of 16 KiB). All AITrainerCardLogic deciders/plays decompiled (Pokedex/Pokemon Recall/Imposter Oak Revenge/Digger landed from disassembly). **Last updated**: 2026-06-03. ### Decompiled regions (named, in source) diff --git a/src/engine/bank08.asm b/src/engine/bank08.asm index 1705bff..cb74129 100644 --- a/src/engine/bank08.asm +++ b/src/engine/bank08.asm @@ -38,7 +38,7 @@ AITrainerCardLogic: ai_trainer_card_logic AI_TRAINER_CARD_PHASE_06, POKEMON_CENTER, AIDecide_PokemonCenter, AIPlay_PokemonCenter ai_trainer_card_logic AI_TRAINER_CARD_PHASE_07, IMPOSTER_PROFESSOR_OAK, AIDecide_ImposterProfessorOak, AIPlay_ImposterProfessorOak ai_trainer_card_logic AI_TRAINER_CARD_PHASE_12, ENERGY_SEARCH, AIDecide_EnergySearch, AIPlay_EnergySearch - ai_trainer_card_logic AI_TRAINER_CARD_PHASE_03, POKEDEX, $5ebd, $5e94 + ai_trainer_card_logic AI_TRAINER_CARD_PHASE_03, POKEDEX, AIDecide_Pokedex, AIPlay_Pokedex ai_trainer_card_logic AI_TRAINER_CARD_PHASE_07, FULL_HEAL, AIDecide_FullHeal, AIPlay_FullHeal ai_trainer_card_logic AI_TRAINER_CARD_PHASE_10, MR_FUJI, AIDecide_MrFuji, AIPlay_MrFuji ai_trainer_card_logic AI_TRAINER_CARD_PHASE_10, SCOOP_UP, AIDecide_ScoopUp, AIPlay_ScoopUp @@ -59,14 +59,14 @@ AITrainerCardLogic: ai_trainer_card_logic AI_TRAINER_CARD_PHASE_04, NIGHTLY_GARBAGE_RUN, AIDecide_NightlyGarbageRun, AIPlay_NightlyGarbageRun ai_trainer_card_logic AI_TRAINER_CARD_PHASE_04, FOSSIL_EXCAVATION, AIDecide_FossilExcavation, AIPlay_FossilExcavation ai_trainer_card_logic AI_TRAINER_CARD_PHASE_12, SLEEP, AIDecide_Sleep, AIPlay_Sleep - ai_trainer_card_logic AI_TRAINER_CARD_PHASE_02, POKEMON_RECALL, $7aa5, $7a90 + ai_trainer_card_logic AI_TRAINER_CARD_PHASE_02, POKEMON_RECALL, AIDecide_PokemonRecall, AIPlay_PokemonRecall ai_trainer_card_logic AI_TRAINER_CARD_PHASE_02, MASTER_BALL, AIDecide_MasterBall, AIPlay_MasterBall ai_trainer_card_logic AI_TRAINER_CARD_PHASE_04, BILLS_TELEPORTER, AIDecide_BillsTeleporter, AIPlay_BillsTeleporter ai_trainer_card_logic AI_TRAINER_CARD_PHASE_02, MOON_STONE, AIDecide_MoonStone, AIPlay_MoonStone ai_trainer_card_logic AI_TRAINER_CARD_PHASE_08, THE_ROCKETS_TRAP, AIDecide_TheRocketsTrap, AIPlay_TheRocketsTrap ai_trainer_card_logic AI_TRAINER_CARD_PHASE_17, GOOP_GAS_ATTACK, AIDecide_GoopGasAttack, AIPlay_GoopGasAttack - ai_trainer_card_logic AI_TRAINER_CARD_PHASE_07, IMPOSTER_OAKS_REVENGE, $7ed4, $7ec3 - ai_trainer_card_logic AI_TRAINER_CARD_PHASE_01, DIGGER, $7f3c, $7f30 + ai_trainer_card_logic AI_TRAINER_CARD_PHASE_07, IMPOSTER_OAKS_REVENGE, AIDecide_ImposterOaksRevenge, AIPlay_ImposterOaksRevenge + ai_trainer_card_logic AI_TRAINER_CARD_PHASE_01, DIGGER, AIDecide_Digger, AIPlay_Digger ai_trainer_card_logic AI_TRAINER_CARD_PHASE_17, COMPUTER_ERROR, AIDecide_ComputerError, AIPlay_ComputerError ai_trainer_card_logic AI_TRAINER_CARD_PHASE_10, SUPER_SCOOP_UP, AIDecide_SuperScoopUp, AIPlay_SuperScoopUp db $ff @@ -4665,6 +4665,238 @@ LookForEnergyUsefulToPlayArea: ret ; 0x21e0e +SECTION "Bank 8@5e0e", ROMX[$5e0e], BANK[$8] + +; Scan the turn duelist's play area for the first Fire- or Lightning-type +; Pokemon, then return (in a) the first card from the prepared wDuelTempList +; whose energy is useful to it; carry set if none found. +LookForUsefulEnergyForFireOrLightningPokemon: + ld a, DUELVARS_NUMBER_OF_POKEMON_IN_PLAY_AREA + get_turn_duelist_var + ld d, a + ld e, $00 +.loop + ld a, DUELVARS_ARENA_CARD + add e + push de + get_turn_duelist_var + call GetCardIDFromDeckIndex + ld a, e + ld [wTempCardID_d0a3], a + ld a, d + ld [wTempCardID_d0a3 + 1], a + call LoadCardDataToBuffer1_FromCardID + pop de + ld a, [wLoadedCard1Type] + or TYPE_ENERGY_FIRE + cp TYPE_ENERGY_FIRE + jr z, .matched + cp TYPE_ENERGY_LIGHTNING + jr nz, .next +.matched + ld [wTempCardType], a + ld hl, wDuelTempList +.scan_energy + ld a, [hli] + cp $ff + jr z, .next + ld b, a + push hl + farcall CheckIfEnergyIsUseful + pop hl + jr nc, .scan_energy + ld a, b + or a + ret +.next + inc e + ld a, e + cp d + jr nz, .loop + scf + ret + +; As above, but for the first Grass-type Pokemon in the play area. +LookForUsefulEnergyForGrassPokemon: + ld a, DUELVARS_NUMBER_OF_POKEMON_IN_PLAY_AREA + get_turn_duelist_var + ld d, a + ld e, $00 +.loop + ld a, DUELVARS_ARENA_CARD + add e + push de + get_turn_duelist_var + call GetCardIDFromDeckIndex + ld a, e + ld [wTempCardID_d0a3], a + ld a, d + ld [wTempCardID_d0a3 + 1], a + call LoadCardDataToBuffer1_FromCardID + pop de + ld a, [wLoadedCard1Type] + or TYPE_ENERGY_FIRE + cp TYPE_ENERGY_GRASS + jr nz, .next + ld [wTempCardType], a + ld hl, wDuelTempList +.scan_energy + ld a, [hli] + cp $ff + jr z, .next + ld b, a + push hl + farcall CheckIfEnergyIsUseful + pop hl + jr nc, .scan_energy + ld a, b + or a + ret +.next + inc e + ld a, e + cp d + jr nz, .loop + scf + ret + +; Pokedex lets the AI look at the top of its deck and reorder those cards. +; The play wrapper forwards the chosen ordering (slots 1-3 plus $d09a/$d09b). +AIPlay_Pokedex: + ld a, [wAITrainerCardToPlay] + ldh [hTempCardIndex_ff9f], a + ld a, [wTempAIMultiTargetCardDeckIndex1] + ldh [hTemp_ffa0], a + ld a, [wTempAIMultiTargetCardDeckIndex2] + ldh [hTempPlayAreaLocation_ffa1], a + ld a, [wTempAIMultiTargetCardDeckIndex3] + ldh [hTempRetreatCostCards], a + ld a, [$d09a] + ldh [hAIEnergyTransPlayAreaLocation], a + ld a, [$d09b] + ldh [$ffa6], a + ld a, $ff + ldh [$ffa7], a + ld a, OPPACTION_EXECUTE_TRAINER_EFFECTS + farcall AIMakeDecision + ret + +; Play Pokedex only occasionally: at most once per 6 considerations +; (wAIPokedexCounter), never when the deck is nearly empty (56+ cards out), +; and then only 30% of the time. When it does, reorder the top 5 cards so +; Pokemon come first, then Trainers, then Energy. +AIDecide_Pokedex: + ld a, [wAIPokedexCounter] + cp $06 + jr c, .no_play + ld a, DUELVARS_NUMBER_OF_CARDS_NOT_IN_DECK + get_turn_duelist_var + cp $38 + jr nc, .no_play + ld a, $0a + call Random + cp $03 + jr c, .reorder +.no_play + or a + ret +.reorder + xor a + ld [wAIPokedexCounter], a + ld a, DUELVARS_NUMBER_OF_CARDS_NOT_IN_DECK + get_turn_duelist_var + add $7e + ld l, a + ld de, $0 + ld b, $05 +.classify_loop + ld a, [hli] + ld c, a + call GetCardTypeFromDeckIndex + push hl + ld hl, wd084 + add hl, de + ld [hl], a + ld hl, wTempAITargetPokemonCardDeckIndex + add hl, de + ld [hl], c + pop hl + inc e + dec b + jr nz, .classify_loop + ld a, $ff + ld [wMusicPanning], a + ld de, wTempAIMultiTargetCardDeckIndex1 + ld hl, wd084 + ld c, $ff + ld b, $00 +.pokemon_pass + inc c + ld a, [hli] + cp $ff + jr z, .trainer_pass + cp $08 ; skip anything that isn't a Pokemon (type < first energy) + jr nc, .pokemon_pass + push hl + ld hl, wTempAITargetPokemonCardDeckIndex + add hl, bc + ld a, [hl] + pop hl + ld [de], a + inc de + jr .pokemon_pass +.trainer_pass + ld hl, wd084 + ld c, $ff + ld b, $00 +.trainer_loop + inc c + ld a, [hli] + cp $ff + jr z, .energy_pass + cp TYPE_TRAINER + jr nz, .trainer_loop + push hl + ld hl, wTempAITargetPokemonCardDeckIndex + add hl, bc + ld a, [hl] + pop hl + ld [de], a + inc de + jr .trainer_loop +.energy_pass + ld hl, wd084 + ld c, $ff + ld b, $00 +.energy_loop + inc c + ld a, [hli] + cp $ff + jr z, .done + and $08 ; energy bit + jr z, .energy_loop + push hl + ld hl, wTempAITargetPokemonCardDeckIndex + add hl, bc + ld a, [hl] + pop hl + ld [de], a + inc de + jr .energy_loop +.done + scf + ret + +; Load the card at deck index a and return its type in a (bc/de preserved). +GetCardTypeFromDeckIndex: + push bc + push de + call GetCardIDFromDeckIndex + call GetCardType + pop de + pop bc + ret + SECTION "Bank 8@5f63", ROMX[$5f63], BANK[$8] AIPlay_FullHeal: @@ -8355,6 +8587,67 @@ AIDecide_Sleep_Deck53: ret ; 0x23a90 +SECTION "Bank 8@7a90", ROMX[$7a90], BANK[$8] + +; Pokemon Recall returns a Pokemon from the play area (and its attached +; cards) to the hand. The play wrapper forwards the chosen target ($ff = +; let the effect pick). +AIPlay_PokemonRecall: + ld a, [wAITrainerCardToPlay] + ldh [hTempCardIndex_ff9f], a + ld a, [wAITrainerCardParameter] + ldh [hTemp_ffa0], a + ld a, $ff + ldh [hTempPlayAreaLocation_ffa1], a + ld a, OPPACTION_EXECUTE_TRAINER_EFFECTS + farcall AIMakeDecision + ret + +; Only the Overflow ($12) and Psychokinesis ($18) decks play Pokemon Recall, +; each to rescue a discarded evolution whose pre-evo is already in hand/play. +AIDecide_PokemonRecall: + ld a, [wOpponentDeckID] + cp OVERFLOW_DECK_ID + jr z, .deck_12 + cp PSYCHOKINESIS_DECK_ID + jr z, .deck_18 + or a + ret +.deck_12 + ld bc, MAGNEMITE_LV12 + ld de, MAGNETON_LV35 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret c + ld bc, SPEAROW_LV13 + ld de, FEAROW_LV27 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret c + ld bc, DODUO_LV10 + ld de, DODRIO_LV25 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret c + ld bc, VOLTORB_LV8 + ld de, ELECTRODE_LV35 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret +.deck_18 + ld bc, ABRA_LV8 + ld de, KADABRA_LV39 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret c + ld bc, KADABRA_LV39 + ld de, ALAKAZAM_LV45 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret c + ld bc, GASTLY_LV13 + ld de, HAUNTER_LV26 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret c + ld bc, HAUNTER_LV26 + ld de, GENGAR_LV40 + farcall LookForCardIDInDiscardPile_GivenCardIDInHandOrPlayArea + ret + SECTION "Bank 8@7b0a", ROMX[$7b0a], BANK[$8] AIPlay_MasterBall: @@ -8864,6 +9157,83 @@ AIDecide_GoopGasAttack: ret ; 0x23ec3 +SECTION "Bank 8@7ec3", ROMX[$7ec3], BANK[$8] + +; Imposter Oak's Revenge makes the OPPONENT shuffle their hand into the deck +; and draw 7 -- a disruption when their hand is large. +AIPlay_ImposterOaksRevenge: + ld a, [wAITrainerCardToPlay] + ldh [hTempCardIndex_ff9f], a + ld a, [wAITrainerCardParameter] + ldh [hTemp_ffa0], a + ld a, OPPACTION_EXECUTE_TRAINER_EFFECTS + farcall AIMakeDecision + ret + +; Only the Eevee Showdown ($48) and Ronald's Power ($6b) decks play it, and +; only when the opposing duelist holds 5+ cards. Eevee Showdown also requires +; a spare duplicate (Switch / Pokemon Trader / Energy Search / Energy +; Retrieval) so it isn't discarding its own only copy; Ronald's Power just +; needs any in-hand duplicate. +AIDecide_ImposterOaksRevenge: + ld a, [wOpponentDeckID] + cp EEVEE_SHOWDOWN_DECK_ID + jr z, .deck_48 + cp RONALDS_POWER_DECK_ID + jr z, .deck_6b +.no_play + or a + ret +.deck_48 + ld a, DUELVARS_NUMBER_OF_CARDS_IN_HAND + call GetNonTurnDuelistVariable + cp $05 + jr c, .no_play + ld de, SWITCH + farcall CheckIfHandHasRepeatedCard + ret c + ld de, POKEMON_TRADER + farcall CheckIfHandHasRepeatedCard + ret c + ld de, ENERGY_SEARCH + farcall CheckIfHandHasRepeatedCard + ret c + ld de, ENERGY_RETRIEVAL + farcall CheckIfHandHasRepeatedCard + ret +.deck_6b + ld a, DUELVARS_NUMBER_OF_CARDS_IN_HAND + call GetNonTurnDuelistVariable + cp $05 + jr c, .no_play + call CreateHandCardList + ld hl, wDuelTempList + farcall FindDuplicateCards_IgnoreTrainerCardToPlay + jp c, .no_play + push af + ld a, [wTempAITargetNonPokemonCardDeckIndex] + cp $ff + jr z, .commit + pop af + ld a, [wTempAITargetNonPokemonCardDeckIndex] + push af +.commit + pop af + scf + ret + +; Digger discards a card to draw 2 -- the AI always plays it when able. +AIPlay_Digger: + ld a, [wAITrainerCardToPlay] + ldh [hTempCardIndex_ff9f], a + ld a, OPPACTION_EXECUTE_TRAINER_EFFECTS + farcall AIMakeDecision + ret + +AIDecide_Digger: + scf + ret + SECTION "Bank 8@7f3e", ROMX[$7f3e], BANK[$8] ; Computer Error's play has its own pre-step: SwapTurn so the