Skip to content

Card-script param declarations (batch 1: framework classes)#11007

Open
MostCromulent wants to merge 14 commits into
Card-Forge:masterfrom
MostCromulent:effect-script-params
Open

Card-script param declarations (batch 1: framework classes)#11007
MostCromulent wants to merge 14 commits into
Card-Forge:masterfrom
MostCromulent:effect-script-params

Conversation

@MostCromulent

Copy link
Copy Markdown
Contributor

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_PARAMS listing 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/getParamOrDefault calls, the defined-resolution helpers (getCards, getDefinedPlayersOrTargeted, …), map lookups, and positional helpers like addToCombat — 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. SpellAbilityAi is the one hand-curated list: as a cross-cutting reader it touches params owned by many effects, so it declares only its own AI* knobs.

The drift test

CardScriptParamDeclarationTest fails 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

MostCromulent and others added 11 commits June 15, 2026 16:06
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",

@tool4ever tool4ever Jun 15, 2026

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.

hardcoding paths is too ugly imo
maybe we can do an IHasForgeParams with getRequired/OptionalParams or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Might have found a way around this after making Claude do another look.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After much prodding of Claude, I think a5fffaf might be closer to what you're after?

MostCromulent and others added 3 commits June 15, 2026 17:33
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>
@MostCromulent MostCromulent marked this pull request as ready for review June 15, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants