Skip to content
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

Rework DynamicValue implementations #12596

Open
wants to merge 83 commits into
base: master
Choose a base branch
from

Conversation

jimga150
Copy link
Contributor

@jimga150 jimga150 commented Jul 23, 2024

#12595

This PR is to get the new pattern established with specific examples.

This will certainly not pass build, but it does demonstrate the solution I've landed on, and I'm presenting this for review before i continue.

I found it will generally be easier to go with the solution @xenohedron proposed here, since there are some values that need to be phrased as if they were an amount, rather than a count (power or toughness, versus creatures on the battlefield, for example).

The big implication of this is that in order to own a multiplier, DynamicValue had to become an abstract class, rather than an interface.

I plan to use this PR to continue implementing this, which will certainly hit every single subclass of DynamicValue as well as many effects that use this type, not to mention cleaning up the following:

1. Any use of toString() on a DynamicValue
2. All uses of .instance on a DynamicValue (since subclasses wont be a singleton anymore)

Many effects will need to be reworked to use this phrasing consistently, after which i don't think there will be that much need for effect text overriding just for the purpose of fixing a dynamic value's text.

Open to feedback.

@jimga150 jimga150 marked this pull request as draft July 23, 2024 00:58
@xenohedron xenohedron self-assigned this Jul 23, 2024
…multiplier behavior and maximize flexibilty with number of/for each phrasing
@jimga150
Copy link
Contributor Author

Skipped the writeup:

This new strategy just focuses on making sure all DynamicValue (now remains an interface) implementations own the phrasing and take that as an argument to getMessage()

Copy link
Contributor

@xenohedron xenohedron left a comment

Choose a reason for hiding this comment

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

some interesting ideas here. solving the boost text is a solid step, since that has the substantial complication of two dynamic/static values together.
I'd like to see an example for a more straightforward effect as well, like DrawCardSourceControllerEffect

@jimga150 jimga150 changed the title Rework CreaturesYouControlCount wrt AngelicExaltation Rework DynamicValue implementations Jul 24, 2024
@jimga150
Copy link
Contributor Author

jimga150 commented Jul 24, 2024

DrawCardSourceControllerEffect now makes use of an enum to switch between the following phrasings:

  1. "draw cards equal to <value>"
  2. "draw X cards, where X is equal to <value>
  3. "draw <multiplier> card/cards for each <value>"

I made the "for each" phrasing the default, since scryfall indicated that that's the most popular phrasing.

Copy link
Contributor

@xenohedron xenohedron left a comment

Choose a reason for hiding this comment

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

make sure to look at the other decorator classes too, not just MultipliedValue

@@ -16,24 +15,38 @@
*/
public class DrawCardSourceControllerEffect extends OneShotEffect {

public enum DrawCardsPhrasing {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like this being nested here - seems like there should be one public enum for all usages

StringBuilder sb = new StringBuilder(youDraw ? "you " : "");
sb.append("draw");
String value = " a";
if (amount instanceof StaticValue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

hm, I wonder if STATIC should be part of the enum, since it'll generally need its own text gen case and that way could just be part of the switch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now, detecting instances of StaticValue is already necessary in Effects when they need to call getValue, so this wouldnt significantly improve the text generation pattern. That is, unless there is a pattern that doesnt involve needing to call instanceof StaticValue?

Copy link
Member

Choose a reason for hiding this comment

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

It's ok to use instanceOf for popular and basic classes. Just make sure it do not break cards with extended class versions.

value = " X";
} else if (drawCardsPhrasing == DrawCardsPhrasing.EQUAL_TO) {
value = "";
} else if (amount instanceof MultipliedValue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

generally not a fan of these instanceof checks, but open to admitting that it's the least bad approach

Copy link
Contributor Author

@jimga150 jimga150 Jul 30, 2024

Choose a reason for hiding this comment

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

This approach is probably going to lead to making a DecoratedValue abstract class (or interface?) for instance checking like this, since all the decorated values will need their qualification put into the text in this manner.

Another possible approach is to make decorator classes and StaticValue inherit a common class or interface (maybe still named DecoratedValue, though having StaticValue inherit that seems wrong to me) that has a getValue() function (or something to descriptively replace toString()) that goes in place here:
Draw <getValue()/""> cards <for each/where X is/equal to> getMessage()

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be possible to have the method in DynamicValue itself

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On second thought, i'm not sure how to avoid the instanceof checks. Technically, some unique behavior could be implemented that identifies instances of MultipliedValue and StaticValue such that they can be handled in places like this, but i'm realizing two things that make this kinda pointless:

  1. StaticValue, MultipliedValue, and the other DynamicValue implementations must all be handled differently (3 broad cases)
  2. I can't think of any other cases than these three. the other decorator classes haven't necessitated property handling like 'MultipliedValue' has. AdditiveDynamicValue and IntPlusDynamicValue only need to modify their getMessage calls, and LimitedDynamicValue and LimitedDynamicValue are used in one place each, so trying to generalize text for them doesn't seem worth it.

So trying to avoid using instanceof will still land us handling these classes specially, at which point why not use instanceof

…enerator for SetBasePowerToughnessSourceEffect for use in AbominationOfLlanowar
…r DamageTargetEffect and static text for SetBasePowerToughnessPlusOneSourceEffect
…itedDynamicValue (used in one place each, so not worth tweaking text generators for)
Copy link
Member

@JayDi85 JayDi85 left a comment

Choose a reason for hiding this comment

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

Also need report with full rules stats in master and in your PR -- how many rules fixed in that PR.

VerifyCardDataTest.test_verifyCards with FULL_ABILITIES_CHECK_SET_CODES = "*"

public String getMessage() {
return this.dynamicValues.stream().map(DynamicValue::getMessage).collect(Collectors.joining(" "));
public String getMessage(EffectPhrasing phrasing) {
if (phrasing == EffectPhrasing.X_HIDDEN){
Copy link
Member

Choose a reason for hiding this comment

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

Use case for phrasing and throw exception on default (e.g. on unsupported)

// return Integer.toString(multiplier);
// }
// StringBuilder sb = new StringBuilder();
// if (multiplier == 2) {
Copy link
Member

Choose a reason for hiding this comment

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

Do not keep commented code or add explanation about it in the first line like "old version" or "TODO: need check all used cards"

StringBuilder sb = new StringBuilder(youDraw ? "you " : "");
sb.append("draw");
String value = " a";
if (amount instanceof StaticValue) {
Copy link
Member

Choose a reason for hiding this comment

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

It's ok to use instanceOf for popular and basic classes. Just make sure it do not break cards with extended class versions.

} else if (toughness.getSign() < 0){
t = "-" + t;
}

// sign fix for zero values
Copy link
Member

Choose a reason for hiding this comment

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

1
Looks like duplicated code for sign values above (new) and below (old). Must use one place to set sign text.

2
Also I don't like "else if" parts -- looks like it will be used for dynamic value only (but your comments about manual sign). Must move to static part instead?

//Non-static values will need their minus sign manually inserted

shot_240731_042908

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like duplicated code for sign values above (new) and below (old). Must use one place to set sign text.

Not sure what the duplicate code youre referring to is. do you mean that within this function, there is more than one statement prepending a minus sign?

Also I don't like "else if" parts -- looks like it will be used for dynamic value only (but your comments about manual sign). Must move to static part instead?

That would be the opposite of the goal for this statement... It's an else if so that only non-StaticValue classes make it there, since they would already have their minus sign from the Integer.toString call. Is the ask to separate any handling of static and dynamic values in this function so they're not interleaved?

Copy link
Contributor Author

@jimga150 jimga150 Aug 15, 2024

Choose a reason for hiding this comment

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

@JayDi85 I'm holding up moving forward with the bulk of implementations for this until we resolve this

@github-actions github-actions bot added the client label Sep 2, 2024
@@ -98,6 +99,11 @@ public BoostAllEffect copy() {
return new BoostAllEffect(this);
}

public BoostAllEffect usingPhrasing(ValuePhrasing textPhrasing) {
Copy link
Contributor

Choose a reason for hiding this comment

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

withTextPhrasing for consistency

Copy link
Contributor

Choose a reason for hiding this comment

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

sorry to be nitpicky, but I think the standard is "with" not "using"

@@ -34,7 +44,6 @@ public IntPlusDynamicValue copy() {
return new IntPlusDynamicValue(this);
}

@Override
Copy link
Member

Choose a reason for hiding this comment

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

Looks like a typo, do not remove override from toString methods:

IMG_0740

sb.append("X");
} else if (textPhrasing == ValuePhrasing.EQUAL_TO) {
// do nothing
} else if (amount instanceof MultipliedValue) {
Copy link
Member

Choose a reason for hiding this comment

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

I don’t like that style of code:

  • it’s full of potential bugs and miss combinations (you combine phrasing param and class type in one switch instead split it);
  • it will be miss on new ValuePhrasing changes (no throw exception on unknown value);
  • it’s overdeveloped - instead using on two lines of code it required 10+ lines.

Old code:
IMG_0741

New code from PR:
IMG_0742

Must use switch with throw error all the time, not “if else”. Current code smells bad. I don’t look at other effects. But if it use/require same logic then whole thing must be rethink/rework.

P.S. Also look at legacy part - it miss empty check in new code. Is that fine?

Copy link
Contributor

Choose a reason for hiding this comment

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

The intricacies of templating require a lot of parameters to generate text correctly. It's not necessarily "overdeveloped".

I agree that a switch would be cleaner, but it is not obvious to me that would in fact be a workable solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also:

  1. the old code doesn't correctly generate text in many common scenarios
  2. if getText method doesn't correctly generate text, it doesn't impact game logic or stability
  3. any discrepancies are easily caught by the verify check

Copy link
Member

Choose a reason for hiding this comment

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

For possible ideas: I like a text template style for complex use cases instead multiple if/else (text replacing is slower, but much more easy to edit and support).

// prepare template
template = “some [VALUE] in the middle and [OTHER PARAM]…”;

// prepare params
realValue = xxx;
realParam = yyy;

// gen text
template.replace(“[VALUE]”, realValue);
template.replace(“[OTHER PARAM]”, realParam);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think i see what you're saying here:

  1. this needs to throw an exception for unsupported phrasings
  2. if-else if chains are inherently less readable and less flexible

I took a shot at changing DamageTargetEffect and DrawCardSourceControllerEffect to use switch statements here. Right now those are the two effects that are being used as a test bed for this pattern.

What remains are the instanceof checks for MultipliedValue and StaticValue complicating such fragments. The base DynamicValue class /could/ be made to own states like being static or having a multiplier, but that would require making it into a class, which we've previously decided against.

If this is still too spaghetti, i'm open to ideas for how this should work instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For possible ideas: I like a text template style for complex use cases instead multiple if/else (text replacing is slower, but much more easy to edit and support).

// prepare template template = “some [VALUE] in the middle and [OTHER PARAM]…”;

// prepare params realValue = xxx; realParam = yyy;

// gen text template.replace(“[VALUE]”, realValue); template.replace(“[OTHER PARAM]”, realParam);

This and xenohedron's comments just showed up for me for some reason--I can get behind this too, but im curious to see if you think the switch statment works first.

Copy link
Member

Choose a reason for hiding this comment

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

but im curious to see if you think the switch statment works first

Yes, new switch is better:

shot_240904_231701

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree that the new switch is better. (Ideally you'd have one switch instead of two switches on that enum, but I'm not sure whether or not trying to combine them would be overall cleaner.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a solid case for the templating idea from JayDi, now that i think about it.

@@ -28,10 +30,18 @@ public DrawCardSourceControllerEffect(DynamicValue amount) {
this(amount, false);
}

public DrawCardSourceControllerEffect(DynamicValue amount, ValuePhrasing textPhrasing) {
Copy link
Member

Choose a reason for hiding this comment

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

For info: I don’t know why devs ignore rules samples in text gen code but it much more easy to read and research with examples like that and/or real card refs:

IMG_0743

@jimga150
Copy link
Contributor Author

jimga150 commented Sep 3, 2024

Do not trust total stats. It can be false positive. If you fixed a text in one popular use case (1000 cards) but broke dozen other not popular use cases (100 cards) then total stats will be +900. And you will never to know about it by stats.

Possible solution: track text stats history after each rule/effect gen change.

I'm already running a complete oracle text check with VerifyCardDataTest with each change, diffing the errors found there with a run from the most recent ancestor commit on master. My understanding is that this runs checks on all cards implemented in Xmage right now in all sets, and catches any changes that resulted in new errors (or resolved existing ones).

Is this not the case? Or is the ask to add some verification in the branch so everyone can review the same check?

@JayDi85
Copy link
Member

JayDi85 commented Sep 3, 2024

Is this not the case? Or is the ask to add some verification in the branch so everyone can review the same check?

Yes. If you checked global stats after each your changes and no degrade found then all fine. No needs in branches and other. Just make sure your changes/replaces really work.

@jimga150
Copy link
Contributor Author

jimga150 commented Sep 7, 2024

The templating is definitely better. Forces me to search for the variations on phrasings that dont necessarily have to do with the DynamicValue at all, like how both DrawCardSourceControllerEffect and DamageTargetEffect both can rearrange parts of their sentences sometimes.

@jimga150
Copy link
Contributor Author

jimga150 commented Sep 19, 2024

@JayDi85 lemme know what you think of the most recent changes to DrawCardSourceControllerEffect and DamageTargetEffect. If the approach looks OK i will move on to implementing the new 'getMessage' for DynamicValues in stages.

@JayDi85
Copy link
Member

JayDi85 commented Sep 19, 2024

How many classes will require changes such as DrawCardSourceControllerEffect?

@jimga150
Copy link
Contributor Author

By the end of this whole rework, all effects that use DynamicValue (My search turns up 108, but there are probably more) will have their text generation adapted to the new phrasing scheme.

Right now however. no effects require changing for this to work. This change maintains backwards compatibility until all the work is done, at which point all effects will have the new phrasing scheme, where the default instance of each effect should generate the same text as its legacy phrasing (albeit using the new templating). Then, i can remove all the legacy text generation (this will be all uses of the ValuePhrasing.LEGACY enum) and remove the old getMessage() calls.

@jimga150
Copy link
Contributor Author

@JayDi85 Lemme know if that sounds acceptable.

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.

3 participants