Skip to content

Check spell timing before scanning for cast-with-flash effects - #11963

Merged
tool4ever merged 1 commit into
Card-Forge:masterfrom
MostCromulent:cast-timing-before-flash-scan
Sep 19, 2026
Merged

tool4ever merged 1 commit into
Card-Forge:masterfrom
MostCromulent:cast-timing-before-flash-scan

Conversation

@MostCromulent

Copy link
Copy Markdown
Contributor

Summary

Priority passes with a non-empty stack are cheaper, because two board-wide scans for "cast as though it had flash" effects no longer run when their result cannot change the answer.

This complements #11946, which removed two other board-wide scans from wide token cascades like Scute Swarm and listed the AI's priority pass among the costs that remained. This change removes the part of that cost that came from these scans.

Cost

StaticAbilityCastWithFlash.anyWithFlash and anyWithFlashNeedsInfo each build a collection of every card in the static-ability source zones and ask each card for its static abilities. Both are reached from SpellAbilityRestriction.canPlay, ahead of the activator check, so they run for every ability a player's priority check looks at, including abilities on other players' permanents that the player could never activate:

  • SpellAbility.canCastTiming calls withFlash, and through it anyWithFlash, whenever the player cannot act at sorcery speed. That includes every pass with an object on the stack. For an ordinary activated ability the scan cannot change the result: activated abilities are instant speed by default, so the method returns true whether the scan finds anything or not.
  • canPlay runs anyWithFlashNeedsInfo first and checks canCastTiming second. The scan only matters when the timing check fails, but it ran for every ability, including the many whose timing already passes.

The AI's priority decision calls canPlay for each ability on its hand and graveyard, the exile and command zones, and every battlefield, on every pass. Each of those calls scanned the whole board twice, so a pass cost grew with the number of abilities times the number of cards, and a long stack paid it once per resolution.

Fix

  • canCastTiming returns true for an activated ability that is neither a planeswalker ability nor sorcery speed, before it reaches withFlash. Spells, planeswalker abilities and sorcery-speed abilities go through the scan as before.
  • canPlay checks canCastTiming first and runs anyWithFlashNeedsInfo only when the timing check fails.

Why the result is unchanged

  • In canCastTiming, the old code already returned true for such an ability on every path, either through withFlash or through the final !isPwAbility() && !isSorcerySpeed() line. Returning earlier skips only the scan. No ability is both a spell and an activated ability, so once that case has returned, the rest reduces to canCastSorcery() || withFlash(...).
  • In canPlay, if (!needsInfo) { if (!timing) return false; } and if (!timing && !needsInfo) return false; are the same condition. Both calls only read game state, so evaluating them in the other order changes nothing.
  • Sorcery speed is fixed when an ability is built. The CastWithFlash statics that target activated abilities (Teferi, The Wandering Emperor, Leonin Shikari and others) all target loyalty or equip abilities, which still go through the scan.

Measurements

Four runs of each build, alternated, from the same seeds. Deltas are the change in the mean.

workload before after delta
Four-player commander precons, 3 games 117546 ms 108358 ms -7.8%
Ordinary constructed, 3 games 3953 ms 3526 ms -10.8%
Scute Swarm landfall, 480 copies 28850 ms 26344 ms -8.7%
Scute Swarm landfall, 480 copies, with Glorious Anthem 45192 ms 43095 ms -4.6%
Scute Swarm landfall, 480 copies, opponent holding untapped lands and an instant 39119 ms 35586 ms -9.0%

Sampling puts the two scans at 8.4% of a four-player commander game before the change and 3.1% after, and at 6.6% and 0.1% of the Scute Swarm runs. What remains in commander is spells and sorcery-speed abilities, where the scan still decides the answer.

Commander and ordinary constructed game logs are identical between the two builds in every run. Games built from token-making decks show no measurable change.


🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tool4ever
tool4ever merged commit c45ce7b into Card-Forge:master Sep 19, 2026
3 checks passed
@MostCromulent
MostCromulent deleted the cast-timing-before-flash-scan branch September 19, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants