-
Notifications
You must be signed in to change notification settings - Fork 93
Make AI be able to use attacks with any energy color combination
In early Pokémon TCG, attack energy requirements followed a consistent pattern, requiring some amount of the same basic energy type and colorless energy. Attacks never required different energy cards for the same attack. This allowed the developers to write simpler code for the AI, assuming this pattern holds. Of course, this is not a desirable functionality when adding custom attacks with any arbitrary energy requirements. As such, even though these attacks can work for the player, the AI will not know how to use them or play the correct energy cards. In this tutorial, we will write a system that generalizes what is already in the game and have attacks work for any arbitrary combination of energy cards.
- Extend energy requirements in WRAM
- Change energy cost calculation
- Create a routine to get energy card needed
- Have AI routines get Energy Card needed
Firstly we will change how the energy requirements are represented in WRAM, which will be the crux of our modifications. Edit src/wram.asm:
; information about various properties of
; loaded attack for AI calculations
wTempLoadedAttackEnergyCost:: ; cdb5
- ds $1
-wTempLoadedAttackEnergyNeededType:: ; cdb6
- ds $1
+ ds NUM_TYPES / 2
wTempLoadedAttackEnergyNeededAmount:: ; cdb7
+ ds NUM_TYPES / 2
+wTempLoadedAttackEnergyNeededTotal::
ds $1
; used for the AI to store various
The attack cost we will represent all energy types in 4 bytes, each byte holds an energy cost in each nybble (higher 4 and lower 4 bits). Alongside it we will also have an array of energy cards still needed, also represented in the same way. Finally wTempLoadedAttackEnergyNeededTotal
will hold the total number of energy cards that the attack will need, which is a handy number to store.
4 bits per type means that there is an implicit limitation of 15 energy cards needed of a given type, which is more than reasonable.
We are consuming more bytes than there was available for these variables in WRAM, so we need to use up free bytes. Edit src/wram.asm again:
; stores the score determined by AI for first attack
wFirstAttackAIScore:: ; cdbf
ds $1
ENDU
- ds $a
+ ds $4
; information about the defending Pokémon and
; the prize card count on both sides for AI:
Now we need to actually populate these arrays with the appropriate information when AI is processing attack costs. This is done in CheckEnergyNeededForAttack
. Edit src/engine/duel/ai/core.asm:
; output:
; b = basic energy still needed
; c = colorless energy still needed
-; e = output of ConvertColorToEnergyCardID, or $0 if not an attack
; carry set if no attack
; OR if it's a Pokémon Power
; OR if not enough energy for attack
CheckEnergyNeededForAttack:
...
jr nz, .is_attack
.no_attack
lb bc, 0, 0
- ld e, c
scf
ret
...
ldh a, [hTempPlayAreaLocation_ff9d]
ld e, a
call GetPlayAreaCardAttachedEnergies
+; fallthrough
+
+CalculateEnergyNeededForAttack:
bank1call HandleEnergyBurn
+ ; fill wTempLoadedAttackEnergyCost
+ ld hl, wLoadedAttackEnergyCost
+ ld de, wTempLoadedAttackEnergyCost
+ ld bc, NUM_TYPES / 2
+ call CopyDataHLtoDE
+
+ ; clear wTempLoadedAttackEnergyNeededAmount
+ ; and wTempLoadedAttackEnergyNeededTotal
+ ld hl, wTempLoadedAttackEnergyNeededAmount
+ ld c, NUM_TYPES / 2 + 1
xor a
- ld [wTempLoadedAttackEnergyCost], a
- ld [wTempLoadedAttackEnergyNeededAmount], a
- ld [wTempLoadedAttackEnergyNeededType], a
+.loop_clear
+ ld [hli], a
+ dec c
+ jr nz, .loop_clear
ld hl, wAttachedEnergies
ld de, wLoadedAttackEnergyCost
- ld b, 0
- ld c, (NUM_TYPES / 2) - 1
-
+ ld c, FIRE
.loop
- ; check all basic energy cards except colorless
- ld a, [de]
- swap a
+ ; check all basic energy cards
call CheckIfEnoughParticularAttachedEnergy
- ld a, [de]
call CheckIfEnoughParticularAttachedEnergy
inc de
- dec c
+ ld a, c
+ cp NUM_TYPES
jr nz, .loop
-; running CheckIfEnoughParticularAttachedEnergy back to back like this
-; overwrites the results of a previous call of this function,
-; however, no attack in the game has energy requirements for two
-; different energy types (excluding colorless), so this routine
-; will always just return the result for one type of basic energy,
-; while all others will necessarily have an energy cost of 0
-; if attacks are added to the game with energy requirements of
-; two different basic energy types, then this routine only accounts
-; for the type with the highest index
+ ; count basic energy cards in use
+ ld hl, wTempLoadedAttackEnergyCost
+ ld de, wTempLoadedAttackEnergyNeededAmount
+ ld c, 0
+ ld a, (NUM_TYPES / 2) - 1
+.loop_tally_energies_in_use
+ push af
+ ld a, [de] ; needed amount
+ swap a
+ and %1111
+ ld b, a
+ ld a, [wTempLoadedAttackEnergyNeededTotal]
+ add b
+ ld [wTempLoadedAttackEnergyNeededTotal], a
+ ld a, [hl] ; energy cost
+ swap a
+ and %1111
+ sub b
+ add c
+ ld c, a
+ ld a, [de] ; needed amount
+ inc de
+ and %1111
+ ld b, a
+ ld a, [wTempLoadedAttackEnergyNeededTotal]
+ add b
+ ld [wTempLoadedAttackEnergyNeededTotal], a
+ ld a, [hli] ; energy cost
+ and %1111
+ sub b
+ add c
+ ld c, a
+ pop af
+ dec a
+ jr nz, .loop_tally_energies_in_use
; colorless
- ld a, [de]
+ ld a, [hl]
swap a
and %00001111
ld b, a ; colorless energy needed overall
- ld a, [wTempLoadedAttackEnergyCost]
- ld hl, wTempLoadedAttackEnergyNeededAmount
- sub [hl]
- ld c, a ; basic energy still needed
ld a, [wTotalAttachedEnergies]
sub c
sub b
jr c, .not_enough
- ld a, [wTempLoadedAttackEnergyNeededAmount]
+ ld a, [wTempLoadedAttackEnergyNeededTotal]
or a
ret z
...
cpl
inc a
ld c, a ; colorless energy still needed
- ld a, [wTempLoadedAttackEnergyNeededAmount]
+ ld a, [wTempLoadedAttackEnergyNeededTotal]
ld b, a ; basic energy still needed
- ld a, [wTempLoadedAttackEnergyNeededType]
- call ConvertColorToEnergyCardID
- ld e, a
- ld d, 0
scf
ret
Also edit CheckIfNoSurplusEnergyForAttack
in the same file:
CheckIfNoSurplusEnergyForAttack:
...
ld e, a
call GetPlayAreaCardAttachedEnergies
bank1call HandleEnergyBurn
+
+ ; fill wTempLoadedAttackEnergyCost
+ ld hl, wLoadedAttackEnergyCost
+ ld de, wTempLoadedAttackEnergyCost
+ ld b, NUM_TYPES / 2
+ call CopyNBytesFromHLToDE
+
+ ; clear wTempLoadedAttackEnergyNeededAmount
+ ld hl, wTempLoadedAttackEnergyNeededAmount
+ ld c, NUM_TYPES / 2
xor a
- ld [wTempLoadedAttackEnergyCost], a
- ld [wTempLoadedAttackEnergyNeededAmount], a
- ld [wTempLoadedAttackEnergyNeededType], a
+.loop_clear
+ ld [hli], a
+ dec c
+ jr nz, .loop_clear
+
ld hl, wAttachedEnergies
ld de, wLoadedAttackEnergyCost
- ld b, 0
- ld c, (NUM_TYPES / 2) - 1
+ ld c, FIRE
.loop
- ; check all basic energy cards except colorless
- ld a, [de]
+ ; check all basic energy cards
+ call CheckIfEnoughParticularAttachedEnergy
+ call CheckIfEnoughParticularAttachedEnergy
+ inc de
+ ld a, c
+ cp NUM_TYPES
+ jr z, .loop
+
+ ; count basic energy cards in use
+ ld hl, wTempLoadedAttackEnergyCost
+ ld de, wTempLoadedAttackEnergyNeededAmount
+ ld c, 0
+ ld a, (NUM_TYPES / 2) - 1
+.loop_tally_energies_in_use
+ push af
+ ld a, [de] ; needed amount
swap a
- call CalculateParticularAttachedEnergyNeeded
- ld a, [de]
- call CalculateParticularAttachedEnergyNeeded
+ and %1111
+ ld b, a
+ ld a, [hl] ; energy cost
+ swap a
+ and %1111
+ sub b
+ add c
+ ld c, a
+ ld a, [de] ; needed amount
inc de
- dec c
- jr nz, .loop
+ and %1111
+ ld b, a
+ ld a, [hli] ; energy cost
+ and %1111
+ sub b
+ add c
+ ld c, a
+ pop af
+ dec a
+ jr nz, .loop_tally_energies_in_use
; colorless
- ld a, [de]
+ ld a, [hl]
swap a
- and %00001111
- ld b, a
- ld hl, wTempLoadedAttackEnergyCost
+ and %1111
+ ld b, a ; colorless energy needed overall
ld a, [wTotalAttachedEnergies]
- sub [hl]
+ sub c
sub b
ret c ; return if not enough energy
Also edit CheckIfEnoughParticularAttachedEnergy
in the same file:
; takes as input the energy cost of an attack for a
; particular energy, stored in the lower nibble of a
; if the attack costs some amount of this energy, the lower nibble of a != 0,
; and this amount is stored in wTempLoadedAttackEnergyCost
; sets carry flag if not enough energy of this type attached
; input:
-; a = this energy cost of attack (lower nibble)
+; c = TYPE_*
+; [de] = attack energy cost
; [hl] = attached energy
; output:
; carry set if not enough of this energy type attached
CheckIfEnoughParticularAttachedEnergy:
+ ld a, c
+ rrca ; carry set if odd
+ ld a, [de]
+ jr c, .no_swap1
+ swap a
+.no_swap1
+
and %00001111
jr nz, .check
.has_enough
inc hl
- inc b
- or a
+ inc c
ret
.check
- ld [wTempLoadedAttackEnergyCost], a
sub [hl]
jr z, .has_enough
jr c, .has_enough
; not enough energy
- ld [wTempLoadedAttackEnergyNeededAmount], a
- ld a, b
- ld [wTempLoadedAttackEnergyNeededType], a
+ push hl
+ push bc
+ ld hl, wTempLoadedAttackEnergyNeededAmount
+ ld b, $00
+ rr c ; /2
+ jr c, .no_swap2
+ swap a
+.no_swap2
+ add hl, bc
+ or [hl]
+ ld [hl], a
+ pop bc
+ pop hl
+
inc hl
- inc b
- scf
+ inc c
ret
An overview of the changes:
- We changed the expected output of this routine, since more than one type of energy card may be needed, we will no longer output the energy card that is needed;
- A new label,
CalculateEnergyNeededForAttack
, will allow us to reuse this code in other parts of the engine; - As discussed,
wTempLoadedAttackEnergyCost
andwTempLoadedAttackEnergyNeededAmount
are filled with 2 types per byte, and the total tally output inwTempLoadedAttackEnergyNeededTotal
.
Since we can now reuse this calculation routine, we can greatly simplify CheckEnergyNeededForAttackAfterDiscard
. Edit src/engine/duel/ai/core.asm again:
CheckEnergyNeededForAttackAfterDiscard:
...
dec [hl]
.asm_1570c
- bank1call HandleEnergyBurn
- xor a
- ld [wTempLoadedAttackEnergyCost], a
- ld [wTempLoadedAttackEnergyNeededAmount], a
- ld [wTempLoadedAttackEnergyNeededType], a
- ld hl, wAttachedEnergies
- ld de, wLoadedAttackEnergyCost
- ld b, 0
- ld c, (NUM_TYPES / 2) - 1
-.loop
- ; check all basic energy cards except colorless
- ld a, [de]
- swap a
- call CheckIfEnoughParticularAttachedEnergy
- ld a, [de]
- call CheckIfEnoughParticularAttachedEnergy
- inc de
- dec c
- jr nz, .loop
-
- ld a, [de]
- swap a
- and $0f
- ld b, a ; colorless energy still needed
- ld a, [wTempLoadedAttackEnergyCost]
- ld hl, wTempLoadedAttackEnergyNeededAmount
- sub [hl]
- ld c, a ; basic energy still needed
- ld a, [wTotalAttachedEnergies]
- sub c
- sub b
- jr c, .not_enough_energy
-
- ld a, [wTempLoadedAttackEnergyNeededAmount]
- or a
- ret z
-
-; being here means the energy cost isn't satisfied,
-; including with colorless energy
- xor a
-.not_enough_energy
- cpl
- inc a
- ld c, a ; colorless energy still needed
- ld a, [wTempLoadedAttackEnergyNeededAmount]
- ld b, a ; basic energy still needed
- ld a, [wTempLoadedAttackEnergyNeededType]
- call ConvertColorToEnergyCardID
- ld e, a
- ld d, 0
- scf
- ret
+ jp CalculateEnergyNeededForAttack
Since CheckEnergyNeededForAttack
doesn't output an energy card anymore, we will create a routine just for that. Edit src/engine/duel/ai/core.asm just before ConvertColorToEnergyCardID
:
+; find out first energy card needed
+; from wTempLoadedAttackEnergyNeededAmount
+GetEnergyCardNeeded:
+ push bc
+ ld hl, wTempLoadedAttackEnergyNeededAmount
+ ld c, FIRE
+.loop_find_type
+ ld a, c
+ rrca ; carry set if odd
+ ld a, [hli]
+ jr c, .no_swap
+ dec hl
+ swap a
+.no_swap
+ and %1111
+ jr nz, .found_type
+ inc c
+ jr .loop_find_type ; we assume this loop will terminate
+.found_type
+ ld a, c
+ pop bc
+; fallthrough
+
; input:
; a = energy type
; output:
; a = energy card ID
ConvertColorToEnergyCardID:
Now we can change AI routines to use GetEnergyCardNeeded
. Edit src/engine/duel/ai/core.asm:
LookForEnergyNeededInHand:
...
ld a, b
or a
jr z, .one_colorless
- ld a, e
+ call GetEnergyCardNeeded
call LookForCardIDInHandList_Bank5
ret c
jr .no_carry
...
LookForEnergyNeededForAttackInHand:
...
ld a, b
or a
jr z, .one_colorless
- ld a, e
+ call GetEnergyCardNeeded
call LookForCardIDInHandList_Bank5
ret c
jr .done
Edit src/engine/duel/ai/energy.asm:
DetermineAIScoreOfAttackEnergyRequirement:
...
ld a, b
or a
jr z, .check_colorless_needed
- ld a, e
+ farcall GetEnergyCardNeeded
call LookForCardIDInHand
jr c, .check_colorless_needed
ld a, 4
...
DetermineAIScoreOfAttackEnergyRequirement:
...
ld a, b
or a
jr z, .check_colorless_needed_evo
- ld a, e
+ farcall GetEnergyCardNeeded
call LookForCardIDInHand
jr c, .check_colorless_needed_evo
ld a, 2
...
AITryToPlayEnergyCard:
...
; in this case, Pokémon needs a specific basic energy card.
; look for basic energy card needed in hand and play it.
- ld a, e
+ farcall GetEnergyCardNeeded
call LookForCardIDInHand
ldh [hTemp_ffa0], a
jr nc, .play_energy_card
Edit src/engine/duel/ai/pkmn_powers.asm:
HandleAIEnergyTrans:
...
ld a, b
or a
jr z, .attack_false
- ld a, e
+ farcall GetEnergyCardNeeded
cp GRASS_ENERGY
jr nz, .attack_false
ld c, b
With that, all the required changes are made. Now the AI can make decisions based on any combination of energy costs.