Skip to content

Max speed activated abilities can be activated at any speed (Howlsquad Heavy and 16 more) #7517

Description

@cuinhellcat

A Max speed — ACTIVATED ability can be activated at any speed, including speed 0. The prefix is parsed, recognized, and then dropped: the ability that reaches the battlefield carries no gate at all.

Reproduction

Howlsquad HeavyMax speed — {T}: Add {R} for each Goblin you control.

Reported from a real game: the Goblin had been taken from its owner, and its new controller — at speed 2 — tapped it for red mana. The owner was at max speed, so the first hypothesis was that the engine reads the owner. It does not read anyone.

Measured through the real action pipeline (GameAction::ActivateAbility, then counting pips in players[].mana_pool), one variable changed per row:

controller's speed owner's speed mana produced expected
4 0 1 1 ✅
2 4 1 0
2 2 1 0

The third row is the one that names the defect: with nobody at max speed the ability still produces mana.

Dumping the parsed permanent confirms it directly — the ability arrives as

kind=Activated  cost=Some(Tap)  condition=None  activation_restrictions=[]

Rules

  • CR 702.178a: "Max speed — [Ability]" means "As long as your speed is 4, this object has '[Ability]'." The glossary spells out whose speed: that permanent's controller (or that card's owner, if it isn't on the battlefield). On the battlefield it is the controller, always.
  • CR 702.179e: a player has max speed if their speed is 4 (CR 702.179d lifts the cap for Gomif-style effects).

Because the ability is absent while the gate is unmet, this is an activation restriction (CR 602.5) — not an intervening-if condition (CR 608.2c + the Shelldock Isle ruling, which this engine deliberately does not use for activation legality).

Where it is lost

parser/oracle.rs, Priority 4 (activated ability). The seam already recovers the typed gate from the em-dash prefix:

let aw_condition = strip_ability_word_with_name(cost_text)
    .and_then(|(aw_name, _)| ability_word_to_condition(&aw_name));

…and then applies it for exactly one prefix:

if matches!(aw_condition, Some(StaticCondition::SourceIsHarnessed)) {
    ir.shell.activation_restrictions.push(ActivationRestriction::SourceIsHarnessed);
}

StaticCondition::HasMaxSpeed is produced by ability_word_to_condition and then discarded. It has nowhere to go anyway: parser/oracle_condition.rs lists HasMaxSpeed in the exhaustive bridge's rejection group ("no exact restriction evaluator"), because ParsedCondition has no max-speed leaf.

This is only the ACTIVATED lane. Max speed — on a trigger (ability_word_to_trigger_condition) and on a static (layers.rsStaticCondition::HasMaxSpeedgame::speed::has_max_speed) are both gated correctly today.

Scope

17 paper cards (Aetherdrift) print a Max speed — activated ability, so it is a class and not a one-off:

Amonkhet Raceway, Avishkar Raceway, Deviant Skytech, Endrider Catalyzer, Gas Guzzler, Glitch Ghost Surveyor, Goblin Surveyor, Hour of Victory, Howlsquad Heavy, Kickoff Celebrations, Leonin Surveyor, Loxodon Surveyor, Muraganda Raceway, Mutant Surveyor, Perilous Snare, Slick Imitator, Starting Column.

Three of the labeled abilities — Endrider Catalyzer, Howlsquad Heavy, Muraganda Raceway — are mana abilities, which is the worst case: they bypass the stack (CR 605.3a), so the ungated mana is simply in the pool with nothing to respond to.

Related but distinct: #1213 (Vnwxt, Verbose Host — the same keyword ungated on a replacement effect) was closed as fixed. That fix did not touch this lane.

Fix

PR attached. Four edits, all inside the existing single-authority seams:

  1. ParsedCondition::HasMaxSpeed — a player-designation leaf, sibling of HasCityBlessing.
  2. oracle_condition.rsStaticCondition::HasMaxSpeed moves from the rejection group to the exactly-representable player-state group.
  3. restrictions.rs — evaluated via game::speed::has_max_speed, the same authority layers.rs uses, so the CR 702.179d exception cannot diverge between the static and the restriction reading.
  4. oracle.rs — the matches!(… SourceIsHarnessed) stamp becomes a small typed helper that distinguishes CR 207.2c ability words (no rules meaning, gate printed in the ability's own text — must stay ungated) from CR 702 keyword prefixes (∞, Max speed — the label is the whole gate).

I deliberately did not make the seam generic over every ability_word_to_condition entry: doing so would gate Threshold/Metalcraft/Delirium/Spell mastery/Revolt/Ferocious activations on a label that CR 207.2c says has no rules meaning, where the printed "Activate only as long as …" clause is already lowered by strip_activated_constraints.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions