Skip to content

Check cheap block requirement before expensive one - #11949

Open
MostCromulent wants to merge 1 commit into
Card-Forge:masterfrom
MostCromulent:lure-check-before-block-cost
Open

MostCromulent wants to merge 1 commit into
Card-Forge:masterfrom
MostCromulent:lure-check-before-block-cost

Conversation

@MostCromulent

Copy link
Copy Markdown
Contributor

CombatUtil.mustBlockAnAttacker builds the set of attackers a blocker is obliged to block. Two of its tests were in the wrong order, because they are not peers: one establishes whether an obligation exists at all, the other asks whether an existing obligation is excused.

attackerLureSatisfied answers the first. If the attacker has no unmet must-be-blocked effect then nothing obliges this blocker, and the attacker is dismissed. It answers from the attacker's own keywords, and it dismisses almost every attacker, because hardly any creature has such an effect.

getBlockCost answers the second. A blocking requirement never forces a player to pay a cost, so where blocking would cost something the requirement is excused. Answering it means walking every card in the static-ability source zones and rebuilding each one's static abilities, and it excuses almost nothing, because block taxes are rare.

The cost question only carries meaning once a requirement is known to exist, so asking it first computed an excuse for obligations that were not there — for every attacker, against every candidate blocker. Asking the cheap question first puts the two in the order the rules pose them, and since both are continue guards over side-effect-free reads, the attackers reaching the later checks are unchanged. The same shape as #11916.

The saving grows with the width of the combat. On sixty distinct creatures a side, the turn with the first attack takes 44800 ms instead of 80189 ms, and calls to CardState.getStaticAbilities over two turns fall from 1007936214 to 362382774. A turn with no combat is unchanged. On a seeded four-player game with stock commander precons the difference is inside run-to-run variance, so no claim is made there beyond the game logs being identical.


🤖 Generated with Claude Code

mustBlockAnAttacker skips an attacker when blocking it costs something, and
again when its lure requirement is already met. The cost check ran first, and
getBlockCost walks every card in the static ability source zones and rebuilds
each one's static abilities, while the lure check only reads the attacker's own
keywords and passes for any attacker without a lure requirement. Both are
side-effect free, so running the cheap one first gives the same answer.

The loop runs per attacker for every candidate blocker, so a wide board pays
the scan for every pair. On a board of sixty distinct creatures a side, one
turn with an attack: 80189 ms to 44800 ms, and calls to
CardState.getStaticAbilities over two turns fall from 1007936214 to 362382774.
A seeded four-player commander table plays the same games two to three percent
faster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@tool4ever tool4ever left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately this optimization may not survive in the future since Hanmac wants to refactor them into statics too anyway: #3307

but I guess we can still take it for now :/

@MostCromulent

Copy link
Copy Markdown
Contributor Author

Further testing, this time with go-wide token decks — AI-vs-AI games between mirrored Goblin token decks, rather than the sixty-creature board above. Three repetitions per arm, arms alternated and the order reversed between reps:

workload before after saving
token decks 41,930 ms 33,920 ms 8,010 ms −19.1%
four-player commander 116,834 ms 113,812 ms 3,022 ms −2.6%
ordinary constructed 3,763 ms 3,523 ms 240 ms −6.4%

The percentages track game length as much as the size of the saving: the three ordinary games total under four seconds, against nearly two minutes for commander.

Measured with the change applied to current master, which includes #11946.

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