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
91 changes: 90 additions & 1 deletion crates/phase-ai/src/mana_colors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
//! (`subtypes` + `abilities`) so a `GameObject` view and a `CardFace` view share
//! a single implementation, mirroring the `*_parts` pattern in `features`.

use engine::ai_support::CandidateAction;
use engine::game::mana_payment::{land_subtype_to_mana_type, outer_cost_color_demand, ColorDemand};
use engine::game::mana_sources::{activatable_mana_actions_for_player, mana_color_to_type};
use engine::types::ability::{AbilityDefinition, AbilityKind, Effect, ManaProduction};
use engine::types::ability::{
AbilityDefinition, AbilityKind, CostCategory, Effect, ManaProduction,
};
use engine::types::actions::GameAction;
use engine::types::game_state::GameState;
use engine::types::identifiers::ObjectId;
Expand Down Expand Up @@ -158,6 +161,92 @@ pub(crate) fn tap_strands_demanded_color(
})
}

/// CR 702.51a (Convoke) / CR 702.126a (Improvise) / Waterbend: whether tapping
/// `object_id` for its Colorless convoke-family marker should be rejected
/// because a currently-legal sibling candidate at this exact `ManaPayment`
/// decision lets `object_id` instead pay a colored pip the pending cast still
/// demands, via its own native mana ability.
///
/// This is zero-cost dominance, not a preference: both actions spend the SAME
/// single tap on the SAME permanent, but the native ability can still cover
/// the trailing generic slot once colored demand clears (or pay the colored
/// pip directly), while the Colorless marker can never retroactively produce
/// a stranded color. Companion to `tap_strands_demanded_color` above — that
/// function fixed this same dead-end bug class for land tap-color selection;
/// this is the convoke-family tap-channel-selection variant: nothing
/// previously preferred a permanent's native colored channel over its
/// Colorless convoke-family marker, so a dual-purpose permanent (e.g. an
/// artifact land that also taps for a color) could be spent via the marker
/// first, permanently stranding a colored pip and dead-ending `ManaPayment`.
pub(crate) fn convoke_native_tap_still_demanded(
state: &GameState,
candidates: &[CandidateAction],
object_id: ObjectId,
) -> bool {
let Some(pending_cast) = state.pending_cast.as_deref() else {
return false;
};
let demand = outer_cost_color_demand(&pending_cast.cost);
if demand == [0u32; 5] {
return false;
}
candidates
.iter()
.any(|c| sibling_native_tap_pays_demand(state, &c.action, object_id, demand))
}

fn sibling_native_tap_pays_demand(
state: &GameState,
action: &GameAction,
object_id: ObjectId,
demand: ColorDemand,
) -> bool {
match action {
GameAction::TapLandForMana { selection } => {
selection.source.object_id == object_id
&& color_is_demanded(demand, selection.mana_type)
}
// Only a tap-cost native ability actually competes for this same tap:
// a tapless ability (e.g. a sacrifice-based mana ability) can still be
// activated AFTER paying the Colorless marker, so it never strands a
// colored pip and must not gate the Colorless action. Use the cost's
// own category classification (CR 118) rather than re-matching cost
// shapes by hand -- it already flattens Composite costs correctly.
GameAction::ActivateAbility {
source_id,
ability_index,
} if *source_id == object_id => state
.objects
.get(source_id)
.and_then(|obj| obj.abilities.get(*ability_index))
.is_some_and(|ability| {
let taps_self = ability
.cost
.as_ref()
.is_some_and(|cost| cost.categories().contains(&CostCategory::TapsSelf));
if !taps_self {
return false;
}
let mut colors = Vec::new();
if let Effect::Mana { produced, .. } = &*ability.effect {
collect_mana_production_colors(&mut colors, produced);
}
colors.iter().any(|&c| color_is_demanded(demand, c))
}),
// CR 702.51a: Convoke (unlike Improvise/Waterbend) offers a colored
// marker per color the creature has, alongside the Colorless one --
// `mana_payment_actions` emits both for the same object. A colored
// `TapForConvoke` on the SAME object is just as dominating a sibling
// as a native land/ability tap: it pays a matching colored pip, so
// the Colorless marker is never the only way to spend this creature.
GameAction::TapForConvoke {
object_id: sibling_id,
mana_type,
} if *sibling_id == object_id => color_is_demanded(demand, *mana_type),
_ => false,
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading