Card-script param declarations (batch 1: framework classes)#11007
Card-script param declarations (batch 1: framework classes)#11007MostCromulent wants to merge 14 commits into
Conversation
Fails the build when a framework or effect class reads a card-script parameter it does not declare in OPTIONAL_PARAMS/REQUIRED_PARAMS, or declares one that appears nowhere in its source. The check is opt-in per class, so declarations can be added a few classes at a time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
||
| // Framework classes that own the shared base layers (their OPTIONAL_PARAMS form the base) | ||
| private static final String[] FRAMEWORK = { | ||
| "forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java", |
There was a problem hiding this comment.
hardcoding paths is too ugly imo
maybe we can do an IHasForgeParams with getRequired/OptionalParams or something?
There was a problem hiding this comment.
Evaluating the IHasForgeParams suggestion:
The interface and the hardcoded-path concern are largely orthogonal. The path list exists for discovery — finding which classes declare params. An interface doesn’t make classes discoverable; enumerating its implementors still needs either a classpath/reflection scan or a hardcoded list. What actually removes the list is discovering declarers by walking the source tree, and that’s independent of how a declaration is stored — static field, annotation, or interface method. So adopting the interface on its own wouldn’t drop the paths.
As the mechanism for a build-time check specifically, the interface also has two frictions. Two of the param owners, AbilityUtils and AbilityFactory, are stateless utility classes that are never instantiated (AbilityFactory is final); their declarations can’t live on an instance method, so they’d stay as static fields — leaving two declaration forms to find and read. And reading declarations through the interface needs instances: the abstract bases (SpellAbilityEffect, SpellAbility, CardTraitBase) can’t be constructed, so a build check would end up reading the returned constant or the source anyway.
Where the interface is clearly the right tool is a runtime-queryable contract — the engine asking a live SpellAbility which params it accepts. There instance methods fit, inheritance covers the abstract bases, and the utilities legitimately don’t participate. If instead the intent is just to drop the hardcoded paths, source-tree-walk discovery does that directly (the test already walks the effects directory; extending it to the framework classes removes the list), with no new dependency, and it crosses the forge-game/forge-ai boundary because it reads files rather than reflecting on classes (forge-game doesn’t depend on forge-ai).
So the deciding question is whether these params need to be queryable from the engine at runtime, or only validated in a build check. The first justifies the interface; the second is solved by discovery alone, independent of declaration form.
There was a problem hiding this comment.
Might have found a way around this after making Claude do another look.
There was a problem hiding this comment.
After much prodding of Claude, I think a5fffaf might be closer to what you're after?
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hardcoded list of framework file paths with a walk over the module source roots, classifying each declarer as base or effect by location. Discovery keys on the field assignment (shared with the parse helpers so they can't drift), tolerating an explicit "new String[]" initializer, and a guard fails loudly if the walk finds nothing rather than passing vacuously. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the IHasForgeParams interface suggested in review: a class that reads card-script params implements it and lists them in its OPTIONAL_PARAMS field (effects also a REQUIRED_PARAMS), so the upcoming card-script linter can ask a class which params it accepts. getOptionalParams/getRequiredParams are default methods that read the runtime class's field via getClass().getField rather than returning it directly. Because the field is static, a direct return on a base class would hand subclasses the base's field; reading getClass()'s field gives each its own layer (or the inherited one when it declares none), so declarers need no per-class override and there is no wrong-layer hazard. The drift test discovers declarers by scanning the classpath for implementors instead of a hardcoded path list. forge-ai classes are off the forge-game test classpath (the dependency runs forge-ai -> forge-game), so its declarers are found by walking source; discovery fails loudly if it comes up empty or misses a known framework class. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batch 1 of the per-class card-script parameter declarations proposed in #10984 (comment).
Summary
This batch covers the framework/base classes rather than individual effects. These classes hold the parameters shared across nearly every ability — costs, conditions, activation/target restrictions, descriptions,
Defined, sub-ability keys, and AI hints — so their params are all optional and together form the base set that every effect inherits. Effect classes (later batches) will declare their own params on top of this base and additionally mark which are required.Each class now exposes a
public static final String[] OPTIONAL_PARAMSlisting the params it reads, making the parameters a class consumes self-documenting.How the params were chosen
The list for each class was produced programmatically, not hand-picked: a regex scanner reads each class's source for the literal forms a param read takes —
getParam/hasParam/getParamOrDefaultcalls, the defined-resolution helpers (getCards,getDefinedPlayersOrTargeted, …), map lookups, and positional helpers likeaddToCombat— and each class declares exactly that set. The same matcher backs the drift test, so the lists are mechanically reproducible rather than a one-off snapshot.Claude then adversarially reviewed the scanner's rules and its output against the source: this is what caught false positives — e.g. defined-values like
"Self"and"Targeted"being mistaken for param names — and tightened the rules.SpellAbilityAiis the one hand-curated list: as a cross-cutting reader it touches params owned by many effects, so it declares only its ownAI*knobs.The drift test
CardScriptParamDeclarationTestfails if a class reads a param it doesn't declare, or declares one that appears nowhere in its source — so the declarations can't silently fall out of sync with the code. It is opt-in per class, so later batches can land a few classes at a time.🤖 Generated with Claude Code