Skip to content

Keep offered casts payable through completion - #1

Merged
nishu-builder merged 12 commits into
mainfrom
codex/coworld-payment-consistency
Aug 27, 2026
Merged

Keep offered casts payable through completion#1
nishu-builder merged 12 commits into
mainfrom
codex/coworld-payment-consistency

Conversation

@nishu-builder

@nishu-builder nishu-builder commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • enter interactive mana payment when a feasible source requires choices that auto-tap cannot make
  • preview fixed discard and life additional costs for targeted spells before advertising the cast
  • preserve CancelCast on the target-selection legal-action fast path
  • add regressions for Rubble Rouser- and Bitter Triumph-shaped failures
  • sync the Rust SetFullControl action across the frontend action union and legacy compatibility adapter
  • sync the weekly Draw-replacement census for Bard, King of Dale
  • import the exact upstream-reviewed Scroll of Fate, Kozilek, the Broken Reality, and Power-up trigger support required by the current published card-data baseline
  • keep the published engine-support gate while ratcheting parser diagnostics against the exact PR base on divergent forks

Verification

  • full hosted CI passes at 8a53342
  • both Rust test shards pass
  • Rust formatting, Clippy, parser, engine-authority, and card-data-load gates pass
  • card-data generation, validation, Draw census, upstream engine-support comparison, and exact-base diagnostic ratchet pass
  • frontend, WASM, Tauri, and lobby checks pass
  • 14 focused manifest integration tests pass, including Scroll of Fate and Kozilek, the Broken Reality
  • 12 focused Power-up integration tests pass, including Marvel Boy, Noh-Varr

nishu-builder and others added 11 commits August 26, 2026 22:49
…hase-rs#7609)

* fix(engine,parser): manifest a card from your hand (Scroll of Fate) (phase-rs#7608)

Mirror the supported cloak from-hand twin (Vannifar) on the manifest
side: a from-hand recognizer arm in the imperative "manifest" dispatch,
a lowering intercept to a ChooseFromZone{Hand} parent with a
Manifest{object_source: ParentTarget} sub-ability, the new
Effect::Manifest.object_source field (serde default None = library-top
source), and a resolver branch manifesting the chosen objects through
the shared morph::manifest_card authority (CR 701.40a vanilla 2/2, no
ward). Rule-13 double parse over the full 35,798-card corpus: exactly
one card changes (scroll of fate). Remaining gap: Kozilek, the Broken
Reality's targeted per-player form (documented in phase-rs#7608).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(engine): pin the from-hand manifest object source to ParentTarget

CodeRabbit round 1: `object_source: Some(_)` only proves the field is
populated — a wrong explicit source would still pass. Both the parser
test and the integration test now match the concrete
`Some(TargetFilter::ParentTarget)` the lowering installs (the value the
ChooseFromZone parent forwards per CR 608.2c), mirroring the Vannifar
cloak integration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(engine): drive Scroll of Fate's registered ability, pin the full manifest profile

Maintainer round 1:
1. The integration test now builds the artifact from its printed Oracle
   text (new scenario helper add_artifact_from_oracle, mirroring the
   enchantment/land builders), activates the registered ability through
   GameAction::ActivateAbility, asserts the {T} cost tapped the source
   (CR 602.2b), resolves off the stack, and answers the
   ChooseFromZoneChoice through GameAction::SelectCards — no synthetic
   parse_effect_chain resolve. Probe: disabling the from-hand parser arm
   makes the activation illegal and the test red.
2. The chosen hand card is now a NONCREATURE (plain sorcery) and the
   complete CR 701.40a profile is pinned: empty name, exactly [Creature]
   (printed Sorcery hidden per CR 708.2a), no super-/subtypes, 2/2,
   ManaCost::NoCost, and no keywords (vs. cloak's ward {2}).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(engine): condition the manifest library-move classification on its source

Maintainer round 2: moves_card_to_or_from_library still returned true
for every Effect::Manifest — the arm's own comment required it to become
conditional the moment a source field arrived. It now mirrors the
neighboring Cloak arm: None (library-top default) stays a library move,
Some(filter) follows filter.extract_zones(), so Scroll of Fate's
from-hand manifest no longer misclassifies (and can no longer reject a
valid mana ability under the CR 605.1a classifier). Discriminating
assertion added alongside the runtime coverage: the from-hand chain
classifies false on both levels (probe: reverting the arm to an
unconditional true turns it red), the library-top control stays true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(engine): harden manifest-from-hand scenario coverage

* fix(engine): correct manifest color assertion citation

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…zilek, the Broken Reality) (phase-rs#7697)

* fix(engine,parser): target players each manifest from their hands (Kozilek) (phase-rs#7691)

Kozilek, the Broken Reality's cast trigger resolved to nothing: everything
around the body already parsed (SpellCast mode, the up-to-two player
multi_target, the repeat_for: TrackedSetSize draw rider), but the predicate
was Effect::Unimplemented.

Built on the Breach the Multiverse shape (per-player ChooseFromZone feeding
one accumulated tracked set), with two leaves added to existing axes:
ZoneOwner::EachTargetedPlayer (iterate the chosen Player targets in APNAP
order, CR 101.4) and Chooser::OwningPlayer (each player picks from their own
hidden hand, CR 608.2c).

Three existing authorities gained one arm each:
- Effect::target_filter() answers Some(Player) for the EachTargetedPlayer
  form so the player slots exist to iterate (all other ChooseFromZone forms
  stay None).
- The multi-target player fan-out excludes that form: it is the missing
  iteration layer for single-player-recipient handlers, and this effect
  already iterates the chosen players itself — left in, it split the chain
  per player so each iteration got its own tracked set.
- manifest.rs gains the tracked-set source arm (Cloak's, minus the exile
  dance) and rebinds to a fresh chain set first, so the "manifested this
  way" rider counts the manifests and not also the picks.

Rule-13 double parse over the full 35,798-card corpus: exactly one card
changes (kozilek, the broken reality). Census pin re-pinned for a line
shift (same producer: WaitingFor::OptionalEffectChoice).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(engine): assert the typed Kozilek chain and pin the APNAP prompt order

CodeRabbit round 1:
- The parser test matched only on the absence of "Unimplemented" in a debug
  string, which an unrelated implemented chain would also pass. It now
  matches the typed chain: ChooseFromZone { count: 2, zone: Hand,
  zone_owner: EachTargetedPlayer, chooser: OwningPlayer }, the up-to-two
  player multi_target spec, the Manifest sub-ability with its TrackedSet
  object source and owner-default entry, and the draw rider with
  repeat_for: TrackedSetSize.
- The integration test branched on the prompted player and so accepted
  either order; it now records the prompt sequence and asserts P0 (the
  active player) is prompted first, per CR 101.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(engine): pin the Kozilek choose's selection mode and tracked-set id

CodeRabbit round 2: the ChooseFromZone match accepted any selection mode
(a regression to random/non-choice selection would have passed, though
each targeted player must CHOOSE — CR 608.2d), and the Manifest match
accepted any TrackedSet id. Both are now concrete:
`selection: CardSelectionMode::Chosen` and
`object_source: Some(TargetFilter::TrackedSet { id: TrackedSetId(0) })`,
so the test proves the manifest consumes THIS choose chain's picks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(engine): parameterize the per-player zone-owner scope

Maintainer round 1 (architecture blocker): EachPlayer / EachOpponent /
EachTargetedPlayer were three serialized siblings for one axis — the
machinery (one parked choice per player in APNAP order, accumulating into
the chain's tracked set) is identical; only the population differs.

They collapse into ZoneOwner::Each(PerPlayerScope) with AllPlayers /
Opponents / TargetedPlayers leaves, and the population resolution moves
into one authority (`per_player_iteration_population`), so a future leaf
is a match arm there rather than a new ZoneOwner sibling. The iteration
gate, the single-owner error arm, and the multi-target fan-out exclusion
all now match on `Each(_)` instead of enumerating siblings.

Serialized-data migration is explicit: ZoneOwner (de)serializes through
ZoneOwnerRepr, which still ACCEPTS the three pre-refactor unit names and
maps them onto the parameterized form, while only ever WRITING
{"Each":<scope>}. Covered by zone_owner_migration_tests; the pre-existing
round-trip pin was updated to the parameterized wire shape and extended
with the TargetedPlayers leaf.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(engine): name the per-player scope after what it iterates

`PerPlayerScope::Opponents` is populated as "every player but the
controller", which is what the only wording that produces it asks for:
"For each other player, ..." (Kaya, Spirits' Justice). CR 102.3 keeps
the two notions apart — a teammate is one of the "other players on
their team" and is not that player's opponent — so resolving this
population through `players::opponents` would drop, in a team format,
exactly the player the card includes.

Rename the leaf to `OtherPlayers` and state at the arm why it
deliberately does not route through `players::opponents`. The
pre-refactor sibling (`EachOpponent`) carried the same misnomer and the
same population, so the legacy wire name keeps deserializing onto it and
stored data is unaffected. A genuinely opponent-scoped population
remains a further leaf in the same match.

Carry verified CR citations on the scope mapping (CR 101.4 for the APNAP
walk, CR 102.3 for other-player vs opponent) and correct a doc paragraph
that still named the pre-refactor variants.

No behavior change: the population, the accepted legacy wire names and
every card's parse are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(engine): make the random per-player zone-owner swallow explicit

`resolve_random_in_chain` reached `resolve_candidate_cards` with the
effect's zone owner and turned its error into an empty pool via
`unwrap_or_default()`. `ZoneOwner::Each` is the one owner with no single
candidate pool — it resolves one prompt per player — so a random
selection over it silently resolved to nothing instead of failing.

No card drives that combination: of the 43 cards whose text carries
per-player wording, none say "at random", and no parser arm emits `Each`
together with `CardSelectionMode::Random`. Rather than build a
speculative per-player random pick, spell the `Err` arm out and put a
`debug_assert!` in the function itself, so a future parse producing the
shape trips the suite instead of resolving to nothing. Release behavior
is unchanged.

Also correct a comment at the iteration gate that still named the
pre-refactor `EachOpponent`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(engine): reject a per-player zone owner in the pool authority

The previous commit put the guard on `resolve_candidate_cards`'s error,
which the tracked-set fast paths never reach: `chain_tracked_set_cards`
returns before the zone owner is read, so a random `ChooseFromZone` over
`ZoneOwner::Each` would have been answered from the whole global or
prior-chain set instead of failing.

Reject `Each` as the pool authority's first act, ahead of both fast
paths — "no single pool" is a property of the owner, not of the caller.
No supported path is affected: `resolve` returns to
`prompt_next_each_player` before reaching the pool authority, and
`resolve_with_choosing_player` is only entered from below that return,
so `Each` reaches this function on no legitimate route. The random
caller keeps its `debug_assert!`, which stays silent across the full
suite (19,547 lib + 5,358 integration).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

Baseline pending for 2dec6c88915db4697706234a7ba2fcedd97b1689 — this populates once main publishes its coverage snapshot (a few minutes after that commit landed).

@nishu-builder
nishu-builder merged commit 439862d into main Aug 27, 2026
12 checks passed
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.

3 participants