Decouple the replacement zone scan from PERFORMANCE_MODE - #11328
Conversation
|
interesting idea, but at the same time pretty over-engineered |
|
On #11058 being the right approach here — one profiling note, in case it's useful for sequencing. I profiled one
The thing that would actually retire the skip — for every event, not just these three — is making that per-card rebuild cheap: either caching the RE list per card the way keywords already are ( |
8bf60d8 to
fd40a42
Compare
PERFORMANCE_MODE gates two unrelated things: the Spell.canPlay LKI re-control skip, which its settings description documents and warns about, and the Tap/Untap/ProduceMana zone skip from Card-Forge#11160, which it does not mention at all. The flag defaults to false and its warning is accurate about the half users are told about, so the scan restriction is inert for almost everyone and is never exercised in CI either. Make the zone restriction unconditional, and take its zones from STATIC_ABILITIES_SOURCE_ZONES rather than a hardcoded battlefield and command pair - the same shape as the fog scan restriction from Card-Forge#11157 a few hundred lines down. A card declaring ActiveZones$ for a graveyard or exile is then still found, which the old skip could not manage: it returned before zonesCheck, so such a declaration was ignored outright. PERFORMANCE_MODE keeps its default and now governs only the Spell.canPlay branch, which is what its description says it does. sim -d "Big 240531" "Big 240531" -n 3 -s 12345, two runs per build, same games throughout (turns 14/9/9, same winners and match scores): master 37804 ms this 29350 ms 1.29x Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The existing test proves a declared ActiveZones$ is honoured, which is true on master too at the default flag setting. This one covers what the skip actually changes: an undeclared replacement is active in every zone, so an untap scan reached one sitting in a library. Fails against master's ReplacementHandler at the default setting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fd40a42 to
88eb248
Compare
|
Rebased onto master - the file is untouched upstream since July, so it replayed clean. Re-verified the pool while I was in there: still 121 I've reframed the body around what I think is the actual point, which it was burying under the benchmark. Of the two things One argument for splitting the flag that I had missed and have also added: its other half is the live workaround for #8801, where casting cards owned by an opponent breaks with Performance Mode on. Telling people to turn it off currently costs them this scan too, since one flag gates both - and #8801 was closed stale rather than fixed, so that is still the situation. This doesn't touch On over-engineered - you're right that the general form is the one to want, and I went to delete the event set and restrict every replacement scan to I also had the safety section wrong and have fixed it. That also gave the branch a test worth having. The one that was here passes on master at the default flag setting, so it only ever guarded the zone set. The new one asserts the narrowing directly - an undeclared I also went and probed the blast radius properly instead of asserting it, hosting an undeclared Re-measured against current master, four rounds with the builds interleaved rather than batched, identical games throughout:
Worth being straight that the increment is smaller than this body claimed. I had it at ~15% on top of #11366; interleaved it is 6.7%. The old figure came from batching all runs of one build together, and this box drifts enough between batches that the batching was doing the talking. #11366 is also a good deal stronger than I had credited it. The real root cause is below both PRs, for what it's worth: Happy to close this if you'd rather have the index or the general form. Just don't want it sitting open if it isn't wanted. |
|
Rewritten since you last looked - +246/-11 across three files became +48/-12 across two, and the engine part is a net line deletion. What changed is at the bottom.
PERFORMANCE_MODEgates two unrelated things: theSpell.canPlayLKI re-control skip, which its settings text documents and warns about, and @Blackvipe99's Tap/Untap/ProduceMana zone skip from #11160, which it does not mention.Only the first is a tradeoff. The second gives up nothing -
zonesCheckalready rejects those zones for every card that declares one, so the skip removes work whose result was thrown away. It ended up behind a flag whose description promises to disable correctness checks, and that is why it is off for almost everyone and never runs in CI.So this isn't a new fast path. It takes the half of
PERFORMANCE_MODEthat was never lossy, makes it the default and unconditional behaviour, and leaves the flag holding only the branch its warning actually describes - one less thing hidden behind "this may break your game".There is a second reason to want them apart, and it costs users today. The flag's other half is the live workaround for #8801 - casting cards owned by an opponent (Praetor's Grasp, Rev, Thief of Sanity, Decadent Dragon, and turn-control effects generally) breaks with Performance Mode on, and the standing advice is to turn it off. That advice currently also throws away this scan's speedup, because one flag gates both, which is what the thread was asking not to have to trade:
After this, turning Performance Mode off to fix the card interaction keeps the tap/untap speedup. #8801 was closed stale rather than fixed, so that tradeoff is still real.
This makes the zone restriction unconditional and takes its zones from
STATIC_ABILITIES_SOURCE_ZONES- the same thing @Blackvipe99 did for the fog scan in #11157, a few hundred lines down in this file.PERFORMANCE_MODEkeeps its default and now governs only theSpell.canPlaybranch, which is exactly what its description says.It also delivers what #11160's comment promised - "in case a custom card wants one active from elsewhere". That skip returned before
zonesCheck, so anActiveZones$declaration was ignored outright; taking the zones from the shared constant means a graveyard or exile declaration now works.Why it isn't the general form
The obvious simplification is to drop the event set and restrict every replacement scan to
STATIC_ABILITIES_SOURCE_ZONES, the wayisPreventCombatDamageThisTurnalready does further down this file. That breaks five cards. The constant deliberately excludesHand(/*, Hand*/), and Loxodon Smiter, Obstinate Baloth, Nullhide Ferox, Wilt-Leaf Liege and Dodecapod all carryR:Event$ Moved | ActiveZones$ Hand. They are untouched here only because the restriction is scoped toTap/Untap/ProduceMana. The fog scan can use the general form becauseDamageDonehas no hand-active case; these three events are the set that does.Measured
sim -d "Big 240531" "Big 240531" -n 3 -s 12345, same seed throughout, so every build plays the same games - all 16 runs below produced identical turn counts and outcomes. Four rounds with the four builds interleaved round-robin rather than batched: this box drifts by tens of percent between batches, and batching quietly credits that drift to whichever build ran last. In-game time, mean of four rounds:Per-round ratios stay inside 1.59-1.63, 2.35-2.46 and 2.50-2.59.
I expected #11366 to subsume this, since the scan's cost is mostly the
getReplacementEffects()rebuild that PR caches. It doesn't - caching makes each visit cheap, this stops ~1700 library and hand cards being visited at all - but the margin is smaller than this body used to claim. On top of #11366 it is worth 6.7% (per-round 5.5-8.0%), not the ~15% I had here before. That older number came from batched runs and I no longer trust it.Safety
Every
TapandProduceManareplacement in the pool declaresActiveZones$- 121Battlefield, 3Command. The 49 declaring nothing are allUntap, all "doesn't untap during your untap step" on a permanent. Nothing needs a zone this skips.The earlier version of this section understated what that means, so to be precise:
zonesChecktreats a missingActiveZones$as active everywhere, not as battlefield-only -so those 49 are formally live in a library too. They are unreachable in practice, because an untap event never targets a card there, but the skip narrows a default-everywhere rather than trimming a declared zone. That is the honest statement of the change.
Blast radius
Probed rather than argued: an undeclared
Untapreplacement hosted in each of the 19ZoneTypes, master against this branch. Seven zones change, not the two this body used to imply -HandandLibrary, plus the five command-adjacent deck zonesSchemeDeck,PlanarDeck,AttractionDeck,ContraptionDeckandJunkyard, which are inPART_OF_COMMAND_ZONEbut not inSTATIC_ABILITIES_SOURCE_ZONES.Battlefield,Graveyard,ExileandCommandare unchanged;Sideboard,Ante,Merged,SubgameandNonewere already unscanned.Flashback,StackandExtraHandthe probe could not construct.The five deck zones are unreachable in the current pool. The 49 undeclared effects sit on ordinary permanents - creatures, artifacts, auras, one land - so none of them can be in a scheme, planar, attraction or contraption deck. The three cards of those types that do carry one of these events (Edge of Malacol, Imprison This Insolent Wretch, Mirri) all declare
ActiveZones$ Command, which already excluded their own deck zone.HandandLibraryare the only zones where the narrowing is observable.The one thing given up: the constant excludes
Hand, so a custom card declaringActiveZones$ Handon these three events would stop working. Nothing in the pool does, and the fog check already has this property. If it matters, widening the shared constant serves all ~30 call sites rather than just this one.Two tests. The first is a no-regression guard for the zone set, and I should be straight that it is only that: at the default flag setting every zone is scanned today, so it passes on master too and discriminates only with the flag on. The second covers the narrowing itself and does fail against master at the default setting - an undeclared replacement is active in every zone, so an untap scan reached one sitting in a library. 356 tests, 0 failures, checkstyle clean.
What changed since the first version
ReplacementScanZonesclass is gone. It parsedEvent$/ActiveZones$out of card script text at startup to derive the zone set - re-implementing the parser next door to the real one, to compute what is a constant. It also missed its own use case: Effect-hosted replacements (False Dawn, Ood Sphere) get their command zone fromEffectEffectin code, not script, so a text scan can't see them.parseReplacementinstead. Better, but still inventing a zone setSTATIC_ABILITIES_SOURCE_ZONESalready meant, and it benchmarked no faster - complexity for nothing.🤖 Implemented with the assistance of Claude Code (Opus 5).