Skip to content

Make AI be able to use attacks with any energy color combination

ElectroDeoxys edited this page Nov 11, 2024 · 1 revision

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.

Contents

  1. Extend energy requirements in WRAM
  2. Change energy cost calculation
  3. Create a routine to get energy card needed
  4. Have AI routines get Energy Card needed

1. Extend energy requirements in WRAM

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:

2. Change energy cost calculation

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 and wTempLoadedAttackEnergyNeededAmount are filled with 2 types per byte, and the total tally output in wTempLoadedAttackEnergyNeededTotal.

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

3. Create a routine to get energy card needed

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:

4. Have AI routines get Energy Card needed

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.