-
Notifications
You must be signed in to change notification settings - Fork 834
WIP - Rework/continuous effects apply logic #13689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
* apply effect is now simpler * when applying a layer, dependency checks are now dynamic * applied effects are also tracked during apply to have the engine handle rule 613.6 instead of cards
* Added method to get if an effect is character defining * Added method to check if sublayer matches to help simplify applying effects * Added filter variables for cards, permanents and stackObjects to help determine objects * Added queryAffectedObjects to get the effects potential set of objects * Added calculateResult which should return an expected value to help determine dependency. (e.g. Necrotic Ooze returns the number of abilities it will gain)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few important notes and possible problems of the new logic
DefaultDirectedGraph<ContinuousEffect, DefaultEdge> dependencyGraph = new DefaultDirectedGraph<>(DefaultEdge.class); | ||
Game gameSim; | ||
try { | ||
gameSim = game.copy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run game sim with game cycle before apply each game’s effect — that’s very bad and super slow. Can be allowed for debug mode only, not for each game cycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you apply only single effect to find a real result then it can be ok, but if you apply all other effects in game sim — that’s exponentially slow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is purely for simulating what happens to the affected objects from applying one effect at a time. This gets called too frequently to try and do a full application. Memoization for dependency order would also be good to prevent recalculating the same set of effects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For current code I'm used GameView/GameState
compare to find out changed objects and characteristics after each effect apply. It was able to construct full layers with all objects and effects in apply order and with all change history (per layer, per effect, per object). Just as proof of work for potential test mode's GUI tool for layers debugging/research.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be useful to have. Instead of copying the game, would bookmarking and doing a gamestate compare with the original game be ok? I was copying the game to be safe, but having a copy of the gamestate before doing changes to restore would probably be fine? I noticed the simulation games don't do anything for restore state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's ok for your tasks here (just copy a game state to compare). When you copy game object then you are create a fully independent "game" and can change data inside it without worry about changes in parent game. If you want to simulate some actions then copy it as game sim -- it's will create independent game object without human interactions in choice dialogs, so you can apply effects and other things to "look ahead".
But do not use "bookmark" code for such tasks -- it's a part of rollback feature and save copy of game state for potential restore on user's request, game error or cancel action. All bookmarked game states store inside current game and copying with it. More bookmarks -- more saved game states and more memory used by a game.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example:
- you have effects list to apply/check;
- copy real game as game sim;
- copy and cycle effects and call apply with copied game param;
- check copied game for changed data;
- real game will be safe;
|
||
FilterStackObject getAffectedStackObjectFilter(); | ||
|
||
FilterCard getAffectedCardFilter(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn’t help to find really affected objects - xmage effects uses “apply” and custom code logic inside it, not filters only. There are 600 effects to rework to support it.
For info: it’s a new forge’s approach (only few months in development) and they do not finish it yet (with performance issues, with mtg rules compatibility). Also their’s approach requires to rework all cards with layer effects too. See main and related issues from Card-Forge/forge#5642 and implementation from Card-Forge/forge#6869 — it’s a good example of required amount of work to support it in xmage too. |
I recommend to split it to 2 big tasks/pr:
|
I can see splitting to |
Just a prudent test. The following cards are played in this order: Opalescence, Humility, Opalescence. They should be 4/4, 4/4/, 1/1. |
Gotcha, I misunderstood what you were saying. You wanted the queryObjects and applyToObjects to be inside of the apply methods? I don't think these are necessary for
|
@JayDi85 is this current implementation more appropriate? |
Permanent licid = source.getSourcePermanentIfItStillExists(game); | ||
if (licid != null) { | ||
public Map<UUID, MageItem> queryAffectedObjects(Layer layer, Ability source, Game game) { | ||
if (!affectedObjectMap.isEmpty()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you call queryAffectedObjects
then it must build affectedObjectMap
every time from scratch. If you catch performance issue or duplication bugs -- then it's the problem of caller code, not queryAffectedObjects
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean, I think I will remove the map from ContinuousEffectImpl. I don't think it will be used outside of apply and dependency checking later. I think in that case it could be changed back to a list of objects. Unless you think it would still benefit from being a map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless you think it would still benefit from being a map?
If you don't need it in existing effects then do not add -- use simple List and cycle code.
P.S. I recommend to view improved approach -- create objects list outside and pass it to query method. So real effects must simply fill it without create/clean code in each effect.
Possible code -- I don't build it, just an idea -- make empty apply method (2 params) in ContinuousEffectImpl
and insert query code in main apply method (4 params). So It must keep workable all old effects, but allow to migrate to new query logic for new/modified effects (you must remove apply method from effect and replace it by query and applyToObject methods).
if (spell != null && spell.getCard().getSecondCardFace() != null) { | ||
affectedObjectMap.put(spell.getId(), spell); | ||
} | ||
return affectedObjectMap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good example of weird code. Original idea -- queryAffectedObjects
must return new lists all the time and do not store it inside effect. Now it can return one list, but store another one.
It's very strange. Maybe you need it for multiple effects call and collect full affected objects list from all effect's calls. I don't know. If true then it must has comments and some research -- how to find and store fully affected list.
this.characterDefining = effect.characterDefining; | ||
this.nextTurnNumber = effect.nextTurnNumber; | ||
this.effectStartingStepNum = effect.effectStartingStepNum; | ||
this.affectedObjectMap.putAll(effect.affectedObjectMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
muse use deep copy
|
||
void applyToObjects(Layer layer, SubLayer sublayer, Ability source, Game game, Map<UUID, MageItem> objects); | ||
|
||
Map<UUID, MageItem> queryAffectedObjects(Layer layer, Ability source, Game game); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New methods must have some docs on usage/override
List<ContinuousEffect> layerEffects = new ArrayList<>(); | ||
for (ContinuousEffect effect : layeredEffects) { | ||
if (timestampGroupName.equals("main")) { | ||
effect.clearAffectedObjectMap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, then I don't understand -- why you need to store full affected objects list instead create it by single call (I don't see any usages of it outside of queryAffectedObjects
+ applyToObjects
cycle
* queryAffectedObjects returns boolean * add docs for applyToObjects and queryAffectedObjects * add default apply(game, source) function, new query logic doesn't need it
@JayDi85 OK, I did some more tweaking. The only thing I'm not completely sold on is |
It's ok to call effects's discard at any moment -- it's just mark effect as outdated, so effect will be removed on next clean up (e.g. on next game cycle). Must be used by effects with non standard duration or if it looks up for some linked object and that's object doesn't exists anymore, so effect is useless. |
* refactored ContinuousEffects starting with "C" and their children * created BecomesXXConstructSourceEffect common effect
# Conflicts: # Mage.Sets/src/mage/cards/a/AettirAndPriwen.java # Mage.Sets/src/mage/cards/b/BlueDragon.java # Mage.Sets/src/mage/cards/k/KondasBanner.java # Mage.Sets/src/mage/cards/t/ThranWeaponry.java # Mage.Sets/src/mage/cards/t/TideShaper.java
* Refactor "D" continuous effects
@Jmlundeen do not modify all cards. Take only few and make it all workable and testable (all tests must work fine too). E.g. game engine must support both code styles (old with apply and new with query). It's will be impossible to review your changes to the cards soon (there are already 200+ files in PR). It's important to keep tests workable while refactor too (if it fail then something broken and must be research before continue). |
Both styles are currently supported and I'm making sure to run the tests as well. Most of these changes are small as well. I'll just hold off on pushing more until I get the greenlight. |
If I understand correctly, this change is to fix the Blood Moon problem of entering permanents not having effects applied until after they hit the battlefield, correct? Will this also fix the problem of casting transformed spells? See |
This is just to rework the logic for how continuous effects are applied by splitting the logic into gathering objects and applying to a list of objects. This will provide setup for adding dynamic dependencies and cleaning up the layers logic after. The "Blood Moon" issue isn't being addressed here. I'm not sure about the transformed spells, but I don't think so, since this is only touching how continuous effects are applied. |
If you're doing a major rework of continuous effects like this, it'd probably be a good idea to consider those problems and, if not fix them immediately, to make it much easier to do so in the future. Both the Blood Moon problem and the Transformed Spells problems are about continuous effects not applying to objects early enough: Blood Moon being permanents entering the battlefield, Transformed Spells being spells entering the stack. I think that'd involve having some way to apply all continuous effects that should apply to a given object that hasn't yet entered the relevant zone. |
@ssk97 from another discussions: original task - replace manual effects dependency to auto-dependency due game sim. It’s big rework and can stuck with many problems. So whole work is to split code by multiple PRs:
|
My point though is that with some minor changes, it should be possible for this change to also fix those two problems. I haven't studied it closely so I might be missing something, but if it had some |
I don't quite understand what 'it' refers to that would have a |
This is a rework for continuous effects to use
queryAffectedObjects
andapplyToObjects
instead ofapply
. This is part 1 of 2 to rework continuous effects layering and dependencies.