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>
Comment thread forge-game/src/test/java/forge/game/ability/CardScriptParamDeclarationTest.java Outdated
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

// The literal forms a card-script parameter read takes in source
private static final Pattern GETPARAM = Pattern.compile(
"(?<![A-Za-z0-9_])(?:getParam|hasParam|getParamOrDefault|getParamOrDefaultBoolean)\\(\\s*\"([^\"]+)\"");

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.

getParamOrDefaultBoolean doesn't exist?

public class AbilityUtils {
public class AbilityUtils implements IHasForgeParams {
public static final String[] OPTIONAL_PARAMS = {
"AbilityCount", "AnnounceMax", "Destination", "ETB", "ForgetOtherTargets",

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.

Destination is effect specific?

*/
public final class AbilityFactory {
public final class AbilityFactory implements IHasForgeParams {
public static final String[] OPTIONAL_PARAMS = {

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.

I'd still like to see additionalAbilityKeys unioned here less manually

public class Cost implements Serializable {
public class Cost implements Serializable, IHasForgeParams {
public static final String[] OPTIONAL_PARAMS = {
"AffectedZone", "Amount", "Announce", "Collected", "CollectedCards", "Color",

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.

would like to see the Hash* fields queried instead if possible

public abstract class SpellAbility extends CardTraitBase implements ISpellAbility, IIdentifiable, Comparable<SpellAbility> {
public static final String[] OPTIONAL_PARAMS = {
"AlternateCost", "Amount", "Announce", "Boast", "CantCopy", "CloakUp", "CostDesc",
"CumulativeUpkeep", "DisguiseUp", "DividedAsYouChoose", "Exhaust", "Hidden",

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.

sometimes, like with CumulativeUpkeep they are only used internally as helper

so for this case if it'd show up in a script it'd actually be wrong

conceptually I'm not sure yet if we need a third category for params that are technically still implemented but very exotic/deprecated instead 🤔

@tool4ever

Copy link
Copy Markdown
Contributor

In general direction seems good.

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.

I want runtime so the Card Workshop can eventually also make partial use of these. 💡

For now I'd still like to see the scope more clearly defined:

  1. I guess we're looking at covering syntax for a pretty good amount of our standard.
    Is there anything we can't detect easily which can at least be noted as "skipped" - consider options for confidence interval + statistics available?

  2. We'll also have some simpler semantics checks (e.g. unused SVar)
    Is this one still easier with Python so we'll have a clean responsibility split - or would it now only be responsible for the PR commenting part?

  3. What parts of 1+2 run via the normal test suite and what only gets included when CI?
    Should we support different failure modes (Warning vs. Error) depending on
    a) execution context
    b) type of logic error
    c) type of script

  4. Before doing more batches devise strategy with the fields in CardScriptParser

  • Imo they should be refactored together to avoid redundancy
  • This might mean MVP of these two PR first
  1. if the parent PR tests more of the syntax via Java does it get much slower? Should it be limited only to /upcoming?

  2. Is there still a use case for keeping the corpus discovery approach? E.g. it might be too messy to also manually define each Params accepted values (+ we're far from being consistent in including "Defined" or "Valid" in their names)
    And sometimes only "True" is valid and others like CombatDamage are actually tri-state...


So the future order could be this:

  1. Java test: IHasForgeParams alignment
  2. Java test: check for valid API
  3. Java test: Linter
  4. Python via CI: compare with Scryfall
  5. Github workflow: post findings from 3.+4. as comments

Might actually be worth a markdown file explaining these workflow steps 🤔

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