Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/actions/abilities/mechanic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub enum AbilityMechanic {
amount: u32,
},
PoisonOpponentActive,
RemoveRandomSpecialConditionFromActive,
HealActiveYourPokemon {
amount: u32,
},
Expand Down
32 changes: 30 additions & 2 deletions src/actions/apply_abilities_action.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::panic;

use log::debug;
use rand::rngs::StdRng;
use rand::{rngs::StdRng, Rng};

use crate::{
actions::{
Expand All @@ -14,7 +14,7 @@ use crate::{
},
effects::TurnEffect,
hooks::is_ultra_beast,
models::{Card, EnergyType, StatusCondition},
models::{Card, EnergyType, PlayedCard, StatusCondition},
State,
};

Expand Down Expand Up @@ -164,6 +164,9 @@ fn forecast_ability_by_mechanic(
amount,
} => discard_energy_to_increase_type_damage(*discard_energy, *attack_type, *amount),
AbilityMechanic::PoisonOpponentActive => poison_opponent_active(),
AbilityMechanic::RemoveRandomSpecialConditionFromActive => {
remove_random_special_condition_from_active()
}
AbilityMechanic::HealActiveYourPokemon { amount } => heal_active_your_pokemon(*amount),
AbilityMechanic::SwitchOutOpponentActiveToBench { .. } => {
switch_out_opponent_active_to_bench()
Expand Down Expand Up @@ -473,6 +476,31 @@ fn poison_opponent_active() -> Outcomes {
})
}

fn remove_random_special_condition_from_active() -> Outcomes {
Outcomes::single_fn(|rng, state, action| {
let active = state.get_active_mut(action.actor);
let conditions = active_special_conditions(active);
if conditions.is_empty() {
return;
}
let condition = conditions[rng.gen_range(0..conditions.len())];
active.clear_status_condition(condition);
})
}

fn active_special_conditions(active: &PlayedCard) -> Vec<StatusCondition> {
[
active.is_poisoned().then_some(StatusCondition::Poisoned),
active.is_paralyzed().then_some(StatusCondition::Paralyzed),
active.is_asleep().then_some(StatusCondition::Asleep),
active.is_burned().then_some(StatusCondition::Burned),
active.is_confused().then_some(StatusCondition::Confused),
]
.into_iter()
.flatten()
.collect()
}

fn coin_flip_sleep_opponent_active() -> Outcomes {
Outcomes::binary_coin(
Box::new(|_, state, action| {
Expand Down
30 changes: 30 additions & 0 deletions src/actions/apply_attack_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ fn forecast_effect_attack_by_mechanic(
*damage_per_heads,
),
Mechanic::SelfHeal { amount } => self_heal_attack(*amount, attack),
Mechanic::HealOneYourPokemon { amount } => heal_one_your_pokemon_attack(*amount),
Mechanic::CoinFlipSelfHeal { amount } => {
coin_flip_self_heal_attack(attack.fixed_damage, *amount)
}
Mechanic::SelfChargeActive { energies } => {
self_charge_active_from_energies(attack.fixed_damage, energies.clone())
}
Expand Down Expand Up @@ -1568,6 +1572,32 @@ fn draw_and_damage_outcome(damage: u32, amount: u8) -> Outcomes {
})
}

fn heal_one_your_pokemon_attack(amount: u32) -> Outcomes {
Outcomes::single_fn(move |_rng, state, action| {
let choices = state
.enumerate_in_play_pokemon(action.actor)
.filter(|(_, pokemon)| pokemon.is_damaged())
.map(|(in_play_idx, _)| SimpleAction::Heal {
in_play_idx,
amount,
cure_status: false,
})
.collect::<Vec<_>>();
if !choices.is_empty() {
state.move_generation_stack.push((action.actor, choices));
}
})
}

fn coin_flip_self_heal_attack(damage: u32, heal: u32) -> Outcomes {
Outcomes::binary_coin(
active_damage_effect_mutation(damage, move |_, state, action| {
state.get_active_mut(action.actor).heal(heal);
}),
active_damage_mutation(damage),
)
}

/// Generic attack that deals bonus damage if the Pokémon has enough energy of a specific type attached.
/// Used by attacks like Hydro Pump, Hydro Bazooka, and Blazing Beatdown.
fn extra_energy_attack(
Expand Down
6 changes: 6 additions & 0 deletions src/actions/attacks/mechanic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ pub enum Mechanic {
SelfHeal {
amount: u32,
},
HealOneYourPokemon {
amount: u32,
},
CoinFlipSelfHeal {
amount: u32,
},
SearchToHandByEnergy {
energy_type: EnergyType,
},
Expand Down
5 changes: 4 additions & 1 deletion src/actions/effect_ability_mechanic_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,10 @@ pub static EFFECT_ABILITY_MECHANIC_MAP: LazyLock<HashMap<&'static str, AbilityMe
"Once during your turn, when you play this Pokémon from your hand to evolve 1 of your Pokémon, you may do 30 damage to your opponent's Active Pokémon.",
AbilityMechanic::DamageOpponentActiveOnEvolve { amount: 30 },
);
// map.insert("Once during your turn, you may remove a random Special Condition from your Active Pokémon.", todo_implementation);
map.insert(
"Once during your turn, you may remove a random Special Condition from your Active Pokémon.",
AbilityMechanic::RemoveRandomSpecialConditionFromActive,
);
map
});

Expand Down
19 changes: 17 additions & 2 deletions src/actions/effect_mechanic_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,15 @@ pub static EFFECT_MECHANIC_MAP: LazyLock<HashMap<&'static str, Mechanic>> = Lazy
Mechanic::CoinFlipDiscardEnergyFromOpponentActive,
);
// map.insert("Flip a coin. If heads, discard a random card from your opponent's hand.", todo_implementation);
// map.insert("Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pokémon by attacks.", todo_implementation);
map.insert(
"Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pokémon by attacks.",
Mechanic::DamageAndCardEffect {
opponent: false,
effect: CardEffect::PreventAllDamageAndEffects,
duration: 1,
coin_flip: true,
},
);
map.insert("Flip a coin. If heads, during your opponent's next turn, prevent all damage from—and effects of—attacks done to this Pokémon.", Mechanic::DamageAndCardEffect {
opponent: false,
effect: CardEffect::PreventAllDamageAndEffects,
Expand Down Expand Up @@ -1853,7 +1861,14 @@ pub static EFFECT_MECHANIC_MAP: LazyLock<HashMap<&'static str, Mechanic>> = Lazy
// map.insert("Heal 10 damage from each of your Pokémon.", todo_implementation);
// map.insert("Heal 20 damage from each of your [P] Pokémon.", todo_implementation);
// map.insert("Heal 30 damage from 1 of your Benched Pokémon.", todo_implementation);
// map.insert("Heal 30 damage from 1 of your Pokémon.", todo_implementation);
map.insert(
"Heal 30 damage from 1 of your Pokémon.",
Mechanic::HealOneYourPokemon { amount: 30 },
);
map.insert(
"Flip a coin. If heads, heal 60 damage from this Pokémon.",
Mechanic::CoinFlipSelfHeal { amount: 60 },
);
// map.insert("Heal 30 damage from each of your Pokémon.", todo_implementation);
// map.insert("If Durant is on your Bench, this attack does 30 more damage.", todo_implementation);
map.insert(
Expand Down
16 changes: 16 additions & 0 deletions src/move_generation/move_generation_abilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ fn can_use_ability_by_mechanic(
!card.ability_used && card.attached_energy.contains(discard_energy)
}
AbilityMechanic::PoisonOpponentActive => _in_play_index == 0 && !card.ability_used,
AbilityMechanic::RemoveRandomSpecialConditionFromActive => {
can_use_remove_random_special_condition_from_active(state, card)
}
AbilityMechanic::HealActiveYourPokemon { .. } => !card.ability_used,
AbilityMechanic::SwitchOutOpponentActiveToBench { require_active } => {
let opponent = (state.current_player + 1) % 2;
Expand Down Expand Up @@ -190,6 +193,19 @@ fn can_use_switch_active_typed_with_bench(
.is_some()
}

fn can_use_remove_random_special_condition_from_active(state: &State, card: &PlayedCard) -> bool {
!card.ability_used
&& state
.maybe_get_active(state.current_player)
.is_some_and(|active| {
active.is_poisoned()
|| active.is_paralyzed()
|| active.is_asleep()
|| active.is_burned()
|| active.is_confused()
})
}

fn can_use_heal_one_your_pokemon_ex_and_discard_random_energy(
state: &State,
card: &PlayedCard,
Expand Down
2 changes: 2 additions & 0 deletions tests/pokemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ mod bonsly_teary_attack_test;
mod camerupt_eruption_test;
#[path = "pokemon/castform_test.rs"]
mod castform_test;
#[path = "pokemon/chansey_blissey_test.rs"]
mod chansey_blissey_test;
#[path = "pokemon/charmeleon_ignition_test.rs"]
mod charmeleon_ignition_test;
#[path = "pokemon/comfey_flower_shield_test.rs"]
Expand Down
172 changes: 172 additions & 0 deletions tests/pokemon/chansey_blissey_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use deckgym::{
actions::{Action, SimpleAction},
card_ids::CardId,
database::get_card_by_enum,
models::{Card, EnergyType, PlayedCard, StatusCondition, TrainerCard},
test_support::get_initialized_game,
};

fn played_card_with_base_hp(card_id: CardId, base_hp: u32) -> PlayedCard {
let card = get_card_by_enum(card_id);
PlayedCard::new(card, 0, base_hp, vec![], false, vec![])
}

fn trainer_from_id(card_id: CardId) -> TrainerCard {
match get_card_by_enum(card_id) {
Card::Trainer(trainer) => trainer,
_ => panic!("Expected trainer card"),
}
}

fn play_will(game: &mut deckgym::Game<'static>, actor: usize) {
game.apply_action(&Action {
actor,
action: SimpleAction::Play {
trainer_card: trainer_from_id(CardId::A4156Will),
},
is_stack: false,
});
}

#[test]
fn test_chansey_bind_wound_heals_one_of_your_pokemon() {
let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();

state.set_board(
vec![
PlayedCard::from_id(CardId::B3127Chansey)
.with_energy(vec![EnergyType::Colorless, EnergyType::Colorless]),
PlayedCard::from_id(CardId::A1001Bulbasaur).with_damage(40),
],
vec![PlayedCard::from_id(CardId::A1053Squirtle)],
);
state.current_player = 0;
state.turn_count = 3;
game.set_state(state);

game.apply_action(&Action {
actor: 0,
action: SimpleAction::Attack(0),
is_stack: false,
});

let (actor, choices) = game.get_state_clone().generate_possible_actions();
assert_eq!(actor, 0);
let heal_bench = choices
.iter()
.find(|choice| {
matches!(
choice.action,
SimpleAction::Heal {
in_play_idx: 1,
amount: 30,
cure_status: false
}
)
})
.expect("Bind Wound should offer a heal choice for damaged benched Pokemon")
.clone();

game.apply_action(&heal_bench);

assert_eq!(game.get_state_clone().get_remaining_hp(0, 1), 60);
}

#[test]
fn test_chansey_scrunch_heads_prevents_next_attack_damage() {
let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();

state.set_board(
vec![PlayedCard::from_id(CardId::A4131Chansey)
.with_energy(vec![EnergyType::Colorless, EnergyType::Colorless])],
vec![PlayedCard::from_id(CardId::A1001Bulbasaur)
.with_energy(vec![EnergyType::Grass, EnergyType::Colorless])],
);
state.current_player = 0;
state.turn_count = 3;
state.hands[0] = vec![Card::Trainer(trainer_from_id(CardId::A4156Will))];
game.set_state(state);

play_will(&mut game, 0);
game.apply_action(&Action {
actor: 0,
action: SimpleAction::Attack(0),
is_stack: false,
});

let mut state = game.get_state_clone();
state.current_player = 1;
game.set_state(state);

game.apply_action(&Action {
actor: 1,
action: SimpleAction::Attack(0),
is_stack: false,
});

assert_eq!(game.get_state_clone().get_active(0).get_remaining_hp(), 100);
}

#[test]
fn test_blissey_happiness_supplement_removes_active_special_condition() {
let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();

state.set_board(
vec![
PlayedCard::from_id(CardId::A1001Bulbasaur),
PlayedCard::from_id(CardId::B3128Blissey),
],
vec![PlayedCard::from_id(CardId::A1053Squirtle)],
);
state.apply_status_condition(0, 0, StatusCondition::Poisoned);
state.current_player = 0;
state.turn_count = 3;
game.set_state(state);

let (_actor, choices) = game.get_state_clone().generate_possible_actions();
let ability = choices
.iter()
.find(|choice| matches!(choice.action, SimpleAction::UseAbility { in_play_idx: 1 }))
.expect("Blissey should be able to cure a condition from the Active Pokemon")
.clone();

game.apply_action(&ability);

assert!(!game.get_state_clone().get_active(0).is_poisoned());
}

#[test]
fn test_blissey_ex_happy_punch_heads_heals_self_after_damage() {
let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();

state.set_board(
vec![PlayedCard::from_id(CardId::PA098BlisseyEx)
.with_energy(vec![
EnergyType::Colorless,
EnergyType::Colorless,
EnergyType::Colorless,
EnergyType::Colorless,
])
.with_damage(80)],
vec![played_card_with_base_hp(CardId::A1001Bulbasaur, 200)],
);
state.current_player = 0;
state.turn_count = 3;
state.hands[0] = vec![Card::Trainer(trainer_from_id(CardId::A4156Will))];
game.set_state(state);

play_will(&mut game, 0);
game.apply_action(&Action {
actor: 0,
action: SimpleAction::Attack(0),
is_stack: false,
});

let state = game.get_state_clone();
assert_eq!(state.get_active(1).get_remaining_hp(), 100);
assert_eq!(state.get_active(0).get_remaining_hp(), 160);
}
Loading