Skip to content

engine: collapse TargetSpec's permanent arms onto the shared filter #825

Description

@ninthworld

Goal

TargetSpec's permanent arms become the shared permanent filter, so a card that targets a
class nobody has targeted before needs no Rust — and the English the server poses for that
slot is generated from the filter rather than hand-written per phrasing.

Context and evidence

Parent: #819, §2.1. Second half; #824 does the non-targeting selectors and introduces the
filter this consumes.

TargetSpec (ability/target.rs:196) has ~29 variants. Counting the target / dealer /
dealt_to values actually used across the 299 catalog files: 20 of the 36 distinct
selectors in the catalog are used by exactly one card.
On the targeting side that includes

  • AnyArtifactEnchantmentOrCreatureWithFlying — Vivien Reid's −3, and nothing else
  • AnyColorlessCreature — Infernal Reckoning
  • AnyTappedCreature — Take Vengeance
  • AnyCreatureDefendingPlayerControls — Star-Crowned Stag
  • AnyPermanentWithManaValue — Isolate
  • AnyCreatureWithFlying — Plummet
  • AnyArtifactCreatureYouControl — Aethershield Artificer
  • AnotherAttackingCreature — Pegasus Courser
  • AnyCreatureOrPlaneswalker — Nicol Bolas
  • AnyLand — Tectonic Rift

Nearly all of them are a product of axes the engine already models: controller relation
(you / opponent / any / another), card type, a keyword read through the computed set,
a combat or tap state, colour or colourlessness, and mana value. That is #824's
PermanentFilter plus a mana-value bound.

Cost of one new variant today — every consuming match is wildcard-free, which is the design
working as intended and also the bill:

Site Arms
sage-engine/src/resolve.rs — legality (CR 601.2c / 608.2b) 31
sage-engine/src/actions/targeting.rs — candidate enumeration 27
sage-server/src/view/prompt.rs — the posed English 29
sage-server/src/rules_text/words.rs 86

plus arms in card/attachment.rs, card/helpers.rs, ability/effect/targets.rs, and
catalog/effects.rs. Two crates, six exhaustive matches, per phrasing.

This does not change the wire contract. TargetRequirement (sage-protocol/src/action.rs)
carries prompt: String and a candidate list; the enum never crosses the socket. What it does
change is where that string comes from. Today prompt.rs:163 is a table of hand-written
phrases — "Choose target player", "Choose target nonland permanent" — one per variant.
Generating an equally natural phrase from a filter is the genuinely hard half of this issue and
is the reason it is separated from #824, not the type plumbing.

exclusions.json names one limit that is a limit of this shape: "Target specs that name a
blocker, or the planeswalker a source is attacking."
The blocking half is one field.

Scope

  • TargetSpec's permanent-selecting arms replaced by one arm carrying the shared
    PermanentFilter, extended with the axes only targeting needs (mana value, attacking,
    tapped, another relative to the source).
  • The non-permanent arms — players, player-or-planeswalker, spells on the stack, cards in a
    graveyard, AnyTarget — stay named. They select different kinds of object and a filter over
    permanents cannot say them.
  • One legality predicate and one candidate enumerator, both reading the filter, replacing the
    parallel matches in resolve.rs and actions/targeting.rs.
  • Prompt text and rules text generated from the filter, in the server, with the phrasing
    quality of the current hand-written table as the bar.
  • Catalog JSON re-authored. No card's rules change.
  • The validator keeps refusing filters that cannot be satisfied or that pair with a payload
    that means nothing.

Acceptance criteria

  • TargetSpec has one permanent arm carrying a filter; the ~24 named permanent variants
    are gone.
  • Legality and candidate enumeration share one predicate — a permanent is a legal target
    exactly when it is a candidate.
  • Every prompt string the server poses today is reproduced or improved. A snapshot test
    over every targeting card in the catalog, reviewed as English, is the evidence.
  • Every rules-text rendering is reproduced or improved, under the same review.
  • A target spec naming a blocking creature is authorable, and the exclusions.json
    entry naming that limit is narrowed to the part still true.
  • Adding a card that targets, say, an untapped enchantment an opponent controls requires no
    Rust change.
  • Hexproof, the CR 608.2b resolution re-check, and up to N slot counts behave exactly as
    before — covered by the existing suites, unchanged.
  • make compat regenerated; make verify green, including make e2e-smoke.

Test and validation evidence

  • The existing behavioural suites are the correctness net: this is behaviour-preserving.
  • A generated-prompt snapshot across every targeting definition in the catalog, checked in and
    reviewed, so a phrasing regression is a diff rather than a discovery in play.
  • The same for rules text.
  • make e2e-smoke and make e2e-views — the prompt is the surface a player reads, and
    AGENTS.md makes the browser tier the gate for exactly this kind of seam.
  • make verify.

Dependencies and sequencing

Blocked by #824, which introduces the filter. Should follow #820 and #822 so the catalog
being re-authored is correct. Independent of #826/#827 in subject matter but overlapping in
files — sequence rather than parallelise.

Estimated scope

Large — broad change requiring a split assessment. Already split from #824; splitting further
would mean landing a state where legality and candidates read different predicates, which is
worse than the size.

Risks and constraints

  • The posed English is the risk, not the types. A generated phrase that reads worse than
    the hand-written one is a real regression in a client AGENTS.md describes as "a dark table,
    not a form", and no automated check can judge it. Per the hard rule, say plainly what could
    not be verified and leave the judgment to the maintainer.
  • A filter with an absent field means "any". A named variant re-authored with a field forgotten
    silently widens what a spell may target — the failure mode is a legal target that should not
    be, which the existing tests will not all catch.
  • The client computes no legality (hard rule) and must keep computing none: the candidate list
    stays server-authoritative and exhaustive.

Non-goals

The non-permanent target kinds. CardFilter and the graveyard selectors. The effect-verb
matrix (#826, #827).

Documentation impact

docs/card-schema.md — the target vocabulary and "How far the vocabulary reaches".
docs/decisions/0004-targeting-model.md — the enumeration contract is unchanged but the
selector it enumerates from is not; check whether the ADR states the shape.
exclusions.json — one entry narrowed. docs/protocol.md only if a prompt's shape changes,
which it should not.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions