Center mana pips, wire AI bracket filter to engine estimate, lean test i18n, collapsible groups - #1382
Conversation
Add a design spec for AiDifficulty::CEDH covering: - Difficulty preset that bypasses 4-player paranoid search scaling - Bracket-5 game-setup lock with AI cascade and warning chip - Combo-recognition skeleton: ComboLine types, ComboDetector trait, ComboRegistry, a ComboLinePolicy gated by difficulty, and a stub CedhMulliganPolicy - Engine-side validate_cedh_bracket as a tag check against the manual-declaration-only CommanderBracketTier::Cedh Scope is intentionally skeleton-only: real combo lines, archetype tuning, backward-chaining synthesis, and multiplayer cEDH gating are explicit non-goals for this phase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update sections 4, 5.3, 5.4, and 7 to reflect the actual TacticalPolicy::activation() and MulliganPolicy::evaluate() gating patterns in the codebase: - ComboLinePolicy gates via activation() returning Some/None keyed off a new DeckFeatures::is_cedh field, not via registration-time difficulty checks (the PolicyRegistry uses a OnceLock shared instance and is registration-stable). - CedhKeepablesMulligan follows the existing *KeepablesMulligan naming convention and gates internally via features.is_cedh inside evaluate(), matching the pattern used by AggroKeepablesMulligan et al. - DeckFeatures gains an is_cedh field populated from the deck's declared CommanderBracketTier at deck-analysis time. Original design intent unchanged; only the integration-point mechanics are corrected against the live trait signatures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bite-sized TDD plan covering 9 phases (~25 tasks): 1. AiDifficulty::CEDH variant + preset + 4p scaling skip 2. DeckFeatures::is_cedh field + population from tier 3. Engine validate_cedh_bracket tag check 4. combo/ module (line + detection + registry stub) 5. ComboLinePolicy gated via activation() 6. CedhKeepablesMulligan gated internally 7. Frontend cedhLock service + dropdown + cascade + filter + chip 8. Engine validation wired into WASM/Tauri/server bridges 9. End-to-end integration test + verification sweep Each task: write failing test, run to confirm fail, write minimal impl, run to confirm pass, commit. Verification uses the Tilt-first pattern from CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds CEDH enum variant (highest ordinal, after VeryHard) with its create_config() match arm: temperature 0.2, depth 3, nodes 96, rollout 2x2, combat_lookahead=true (first tier to enable it), projection_min_budget_ms=1500. WASM caps apply: depth 2, nodes 64. Extends ai_difficulty_serde_roundtrips to cover CEDH. Adds cedh_preset_values and cedh_preset_wasm_caps_apply tests. draft-wasm/bot_ai.rs: CEDH uses same full-evaluation pick strategy as VeryHard. Tasks 1.1 and 1.2 batched into one commit: the pre-commit hook requires builds to pass and a standalone Task 1.1 (missing match arm) would fail clippy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cEDH is exclusively played at 4-player tables and is calibrated for that count from the base preset. The generic 3-4p scaler (cap depth at 2, reduce nodes to 2/3) would cripple it. Non-cEDH difficulties continue to follow paranoid scaling unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tunable bonuses for combo-progress prior boosts. Consumed by ComboLinePolicy (next phase). Defaults +15.0 (this turn) and +5.0 (next turn) match the spec; both serde-default so older saved configs deserialize cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update create_config doc comment (Five->Six presets, drop stale VeryHard reference). - Invert the cEDH bypass in create_config_for_players to avoid an empty-if + non-empty-else (CLAUDE.md idiom rule). - Annotate projection_min_budget_ms=1500 with its interaction with AI_SEARCH_TIME_BUDGET_MS. - Add cedh_skips_paranoid_scaling_at_3p test (the spec range is 3..=4; we previously only covered 4). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds is_cedh: bool as a declaration-derived field on DeckFeatures. Unlike the other structurally-detected fields, is_cedh is set from the declared CommanderBracketTier at deck-analysis time, not from card text. Introduces DeckFeatures::analyze(deck, tier) as the canonical constructor, consolidating the previously-inline feature-detection logic from session::features_for() into a single, testable site. features_for() now delegates to analyze() with CommanderBracketTier::Core as the default until PlayerDeckPool carries explicit tier metadata. ComboLinePolicy::activation() and CedhKeepablesMulligan gate on this field (next phases). Tasks 2.1 + 2.2 batched into one commit: adding is_cedh to the struct literal in session.rs requires updating session.rs in the same changeset. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add bracket_tier: CommanderBracketTier field to PlayerDeckPool (default: CommanderBracketTier::Core via new Default impl on the enum). - Add bracket_tier to PlayerDeckPayload (serde default for backward compatibility; PlayerDeckPayload now derives Default). - Populate bracket_tier from payload at load_deck_into_state and deck_payload_from_current_pools, so Bo3 game 2/3 carries the same declared tier as game 1. - Update features_for(deck, tier) to accept the tier parameter; from_game reads pool.bracket_tier and forwards it, so DeckFeatures::is_cedh is set correctly in production. - Update from_single_deck and ensure_player_features to accept tier; analysis paths (classify_deck_js, starter decks, search.rs) pass CommanderBracketTier::Core since those paths have no declared tier. - Test fixtures use ..Default::default() / ..PlayerDeckPool::default() to inherit the new field's default without churn. Closes the gap from 356f9c5 where features_for hardcoded CommanderBracketTier::Core — without this, ComboLinePolicy and CedhKeepablesMulligan would never activate in production. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- match_flow::deck_payload_from_current_pools now propagates bracket_tier for AI seats (>= 2), not just player/opponent. Prevents silent tier drop in Bo3 multi-AI cEDH games. Adds test: deck_payload_from_current_pools_propagates_ai_seat_bracket_tier. - Remove the trivial features_for passthrough in session.rs; callers call DeckFeatures::analyze directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The plan template used CoreCommander / UpgradedCommander but the actual enum variants in crates/engine/src/game/bracket_estimate.rs are Core / Upgraded. Updated all code blocks and string-tier examples in the plan to match. No-op for already-implemented phases (the implementers correctly used the live names). Prevents future agents from copy-pasting invalid names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cEDH is manual-declaration only per bracket_estimate.rs:18-27 (Cedh is never returned by the estimator algorithmically). validate_cedh_bracket asserts every deck has its tier explicitly set to CommanderBracketTier::Cedh. Returns typed CedhBracketError::DeckNotCedh on the first offender (identified by seat_index). Empty input is Ok (vacuously true). Note: the plan named the error BracketViolation but that type already exists in bracket_estimate.rs with a different meaning (per-axis estimator ceiling crossings). The new type is named CedhBracketError to avoid ambiguity while keeping clear intent. Called from the WASM/Tauri/server bridges in Phase 8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Change CedhBracketError seat_index to u8 to match the
codebase convention (PlayerId(u8), server-core/protocol.rs,
draft-wasm seat APIs).
- Add Display impl to CommanderBracketTier and use {} (not
{:?}) in CedhBracketError's error message. Display is the
long-term contract; Debug worked only by coincidence.
- Rename validate_rejects_empty_input -> validate_accepts_empty_input
(the test asserts Ok(()); the old name read backwards).
- Move legality.rs cEDH-block use statements to the top-of-module
use block.
- Keep CedhBracketError as an enum (not a struct): doc comment
names anticipated future variants (AllDecksUnconfigured,
TableSizeMismatch) expected once Phase 8 wires the validation
into the game-init boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- combo/line.rs: ComboLine, ComboPiece, ComboStep, WinKind, ComboReachability, CardPredicate, ComboLineId — pure types, no game-state coupling. - combo/detection.rs: ComboDetector trait + DefaultComboDetector — walks line.pieces, checks zone presence, computes mana shortfall via zone_eval::available_mana. InLibrary pieces treated as tutorable-but-absent, elevating those lines to ReachableNextTurn. - combo/registry.rs: ComboRegistry with one synthetic stub line (__cedh_stub_test_creature__) for end-to-end wiring proof. Real cEDH lines (Thoracle/Consult, Kiki/Twin, Heliod/Ballista) land in a follow-up phase once card coverage stabilises. 5 tests added, all passing. Clippy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove wrong CR 726 annotation on WinKind::InfiniteLoop (CR 726 is The Initiative, not infinite loops). Replace with a description-only doc; CLAUDE.md forbids unverified CR numbers. - Rename DefaultComboDetector -> StructuralComboDetector; avoids collision with the Default trait connotation. - Unify ReachableThisTurn branches in assess() — both branches produced identical action vecs and differed only in a value that the cast already produces correctly. - Rename test combo_piece_equality_is_structural -> combo_piece_eq_respects_zone for clarity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PolicyId variants ComboLineProgress + CedhKeepablesMulligan. - ComboLinePolicy implementing TacticalPolicy. activation() returns None unless features.is_cedh; verdict() consults ComboRegistry and boosts combo-progressing actions. - Registered in PolicyRegistry::default(). Non-cEDH decks pay zero cost via activation() short-circuit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Prefix PolicyReason strings with combo_line_ to match the convention used by other policies (combat_tax_*, tribal_lord_*, etc.) — makes per-policy trace filtering unambiguous. - Remove orphaned PolicyId::CedhKeepablesMulligan; Phase 6 adds it back alongside the implementation. Avoids an enum variant with no registered policy. - Add TODO(cedh-perf) marker on the reachable_lines call in verdict() — caching by (quick_state_hash(state), ai_player) per spec section 5.3, deferred until real combo lines land. - Switch policies/combo_line to pub(crate) mod for consistency with sibling internal policy modules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add PolicyId::CedhKeepablesMulligan (re-added; was removed in Phase 5's quality pass as an orphan). - Implement CedhKeepablesMulligan in policies/mulligan/cedh_keepables.rs. Internal gate on features.is_cedh; non-cEDH decks see a zero-delta Score (no-op). - When cEDH: <2 lands, >4 lands, or no ramp+tutor+interaction -> ForceMulligan. Otherwise +1.0 baseline keep. - Card classification is name-based against the canonical cEDH staple set (stub heuristic, refined later when ComboRegistry is populated). - Registered in MulliganRegistry::default(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two missing test paths for CedhKeepablesMulligan: - A hand with 2-4 lands and no fast-mana/tutor/interaction triggers ForceMulligan with the expected reason. - A hand with 2-4 lands and at least one fast-mana card passes the baseline-keep gate (+1.0 Score). Closes the coverage gap flagged in Phase 6 spec review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing test for the cedh_keepables_too_many_lands ForceMulligan branch (>4 lands). Closes the third coverage gap flagged in Phase 6 quality review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
anyAiOpponentIsCedh / applyCedhCascade / isDeckCedhLegal. All cEDH-lock decisions in the frontend route through this module. Pure functions; never mutate inputs. Uses numeric CommanderBracket (1-5, cEDH = 5) to match existing AiDeckCandidate.bracket convention. 9 tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 7.2: Selecting cEDH on any AI seat cascades all other AI seats to cEDH via applyCedhCascade(). Fires an inline notice when the cascade triggers (no external toast lib — uses local state with 6s auto-dismiss). When any seat is cEDH, the random AI deck pool is filtered to bracket 5 only. Task 7.3: 'CEDH' added to AI_DIFFICULTIES constant. B5 lock badge (aria-label="B5 lock") renders on the AiDifficultyDropdown when the selected difficulty is CEDH. 3 new tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 7.4: filterByBracket(decks, tier) — pure filter; null passes through all decks, non-null restricts to matching bracket. 3 new tests. AiOpponentConfig's filteredDecks memo already calls filterByBracket via CEDH_BRACKET when anyCedh. Task 7.5: GameSetupPage reads human deck bracket via loadSavedDeckBracket, reads aiSeats from preferencesStore, and renders a yellow warning chip (role="alert") when any AI is cEDH and the human deck is not bracket 5. Non-blocking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the two test gaps flagged in Phase 7 review: - AiOpponentConfig cascade test verifies selecting cEDH on any seat upgrades all other AI seats to cEDH. - GameSetupPage tests verify the warning chip renders when human deck is non-cEDH + any AI is cEDH, and hides otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AiOpponentConfig: store cascade timer in a ref and clear it in a cleanup useEffect so the 6s auto-dismiss does not outlive component unmount. Extract CASCADE_NOTICE_MS const. - Replace inline d.bracket === CEDH_BRACKET in filteredDecks with a call to the exported filterByBracket helper, so the abstraction has a real caller. - AiSeatPanel: render the B5 lock badge alongside the inline difficulty <select> when the seat is set to cEDH (the standalone AiDifficultyDropdown is no longer wired into production after PlayPage's removal — pre-existing dead code, separate cleanup). - Correct stale 'below the select' comment in AiDifficultyDropdown (badge is positioned above). - Note the AiSeatPref[] / CommanderBracket signature deviation in cedhLock.ts JSDoc. - Add tests asserting B5 badge renders on cEDH and hides otherwise in AiSeatPanel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lidation Add `bracket_tier: CommanderBracketTier` (serde default = Core) to `PlayerDeckList` and `DeckData` so the WASM/server wire formats carry tier information through to `PlayerDeckPayload`. `resolve_deck_list` now threads each seat's tier instead of hardcoding Core. In `initialize_game` (engine-wasm), call `validate_cedh_bracket` before `start_game` whenever any seat declares `Cedh` tier; return a typed `cedh_bracket_violation` error when a seat's tier doesn't match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bridges Apply the same cEDH bracket validation pattern to the Tauri desktop bridge (`commands.rs`): validate before `load_and_hydrate_decks` when any seat declares Cedh tier. In server-core, thread `DeckData.bracket_tier` through `resolve_deck` into `PlayerDeckPayload` instead of relying on its default value. Fix struct literal initializers in tests to include the new field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… plumbing Thread `bracket_tier` from localStorage bracket sidecar through the `DeckListPayload` wire format in `GameProvider`. Add helpers `bracketToEngineTier()` and `loadActiveDeckBracket()` to map numeric CommanderBracket (1-5) to engine tier strings. `buildLocalAiDeckList` now preserves per-candidate bracket rather than always defaulting Core. In `GamePage`, intercept `onNoDeck` errors containing "not declared cEDH" and show a blocking `BracketViolation` modal with a "Return to setup" button that navigates to /setup instead of auto-navigating away. Tests: 4 new Vitest specs covering modal render, non-cEDH passthrough, no-error idle state, and navigation on dismiss. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously gated on payload.*.bracket_tier == Cedh, which triggered a spurious bracket-violation error any time a user had a bracket-5 tagged deck — even when playing against a non-cEDH AI difficulty. The spec intent is for validation to fire only when the game itself is cEDH (i.e., any AI seat has AiDifficulty::CEDH). - Add `ai_difficulties: Vec<String>` to `DeckList` and `DeckPayload` (both with `#[serde(default)]` for backward compat). Thread from frontend through WASM bridge. - Extract `any_ai_difficulty_is_cedh` canonical helper into `engine::database::legality` — single authority, tested. - Use the helper in both `engine-wasm/src/lib.rs` and `client/src-tauri/src/commands.rs` as the gate signal. - Derive `Default` on `DeckPayload`, `DeckList`, and `PlayerDeckList` to simplify test construction and silence exhaustive-struct errors across the workspace. - Add regression test: bracket-5 human vs. non-cEDH AI is allowed (the bug that was almost shipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Engine-worker preserves the cedh_bracket_violation flag from the WASM error envelope and sends it via a new optional bracketViolation field on the error response message type. - Engine-worker-client creates an AdapterError with code BRACKET_VIOLATION when the flag is present, so callers can match by typed code rather than by string substring on the error message. - GameProvider detects BRACKET_VIOLATION and passes a typed boolean flag to onNoDeck(reason, bracketViolation). The onNoDeck signature is updated accordingly. - GamePage.handleNoDeck matches on the bracketViolation flag to trigger the modal — no longer string-matches on "not declared cEDH", which was brittle. - p2p-adapter DeckListPayload type carries bracket_tier on each seat and ai_difficulties at the top level so guest decks do not silently drop tier in TS-typed contexts. - AdapterErrorCode.BRACKET_VIOLATION added to types.ts. - GamePage.bracketViolation.test.tsx updated: modal now verified via typed flag, not string substring; new regression test confirms flag=false does not trigger modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # client/src/components/menu/AiDifficultyDropdown.tsx # client/src/components/menu/AiOpponentConfig.tsx # client/src/constants/ai.ts # client/src/pages/GameSetupPage.tsx # crates/engine-wasm/src/lib.rs # crates/phase-server/src/main.rs # crates/seat-reducer/src/tests.rs # crates/server-core/src/session.rs
- CRITICAL: combo detector computed mana shortfall with i32 saturating_sub,
which floors at i32::MIN — `2 - 3 = -1` cast to u8 became 255, making the
policy treat an affordable combo as unreachable. Use unsigned subtraction
so it saturates at 0.
- HIGH: Tauri adapter only mapped the DeckNotCedh bracket error to the typed
BRACKET_VIOLATION; TooManyPlayers ("limited to 4 seats") bypassed it. Match
both Display messages so table-size violations show the blocking modal too.
- MEDIUM: guard combo piece lookups with players.get() instead of direct
index — an eliminated player would otherwise panic.
- MEDIUM (R2): replace DeckFeatures::is_cedh bool with bracket_tier:
CommanderBracketTier. Consumers gate on `== Cedh`; the full tier keeps the
design space open for future bracket-aware behavior.
- MEDIUM: drop the redundant bracket_tier parameter on
resolve_player_deck_list — PlayerDeckList already carries it, removing the
sync hazard.
- MEDIUM: correct the stale "1 line" perf comment in ComboLinePolicy (the
registry now holds 3 lines).
- MEDIUM: document why validate_cedh_bracket carries no CR annotation — the
Commander Bracket system is WotC format guidance (2024+), not the
Comprehensive Rules, so a CR number would be fabricated.
The "overly broad combo heuristic" comment was already resolved earlier:
verdict() matches candidates against the line's resolved required_actions and
gates on missing_mana == 0. Caching reachable_lines() remains a documented
follow-up (per-call cost is still small at 3 lines).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The merge from main brought in the #511/#512 parser fix that corrects Thorin, Mountain-King's category-2/3 anaphoric "its" misparse, so the card no longer retains ObjectScope::Anaphoric in the exported card data. The freeze guard is a tripwire designed to fail exactly here; per its own instructions, remove the card from ANAPHORIC_SCOPE_CARDS and drop both count assertions 265 -> 264. Surfaced after merge because the pre-push hook runs parser::oracle::tests but not the engine integration suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design for a ForceKeep mulligan verdict that stops the cEDH AI from mulliganing below 4 cards, with CR 103.5 free-first card-count math. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose a public `kept_hand_size_after(mulligan_count, free_first)` helper in `crates/engine/src/game/mulligan` so cEDH AI logic can query how many cards a player would keep before committing to another mulligan, without needing to reach into the private `bottom_count_for` function. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Starting hand size and the kept-hand free-first discount are governed by CR 103.5 / 103.5c, not CR 103.4 (starting life total). Fix the STARTING_HAND_SIZE constant comment and the kept_hand_size_after doc annotation, and add boundary tests exercising the saturating_sub floor where the kept hand reaches zero cards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `MulliganScore::ForceKeep` variant with three-way registry precedence (ForceKeep > ForceMulligan > score sum), and implement a hard card-count floor in `CedhKeepablesMulligan` that emits `ForceKeep` when one more mulligan would leave fewer than 4 cards, covering both free-first and normal London mulligan rules via `engine::game::mulligan::kept_hand_size_after`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a registry-level integration test wiring the real `CedhKeepablesMulligan` alongside a `ForceMulligan` policy with cEDH features at a floor-engaged mulligan count, proving the real cEDH floor `ForceKeep` overrides a real `ForceMulligan` through the registry. Move `CEDH_MULLIGAN_FLOOR` below the use groups so the imports stay contiguous. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…idate
ChooseFromZoneConstraint serialized externally-tagged
(`{ "DistinctCardTypes": {...} }`), but the frontend CardChoiceModal reads it
internally-tagged on `constraint.type`. The mismatch left `type` undefined, so
the confirm button never enabled for any selection under a DistinctCardTypes
constraint (Atraxa, Grand Unifier). Add `#[serde(tag = "type")]` to match the
sibling SearchSelectionConstraint convention and the frontend contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ficulty Selecting cEDH on any AI seat used to cascade-lock every opponent to cEDH, so multi-opponent games couldn't mix difficulties without dropping to one opponent. Replace the per-seat "CEDH" difficulty with a table-wide `cedhMode` toggle at the top of the AI opponents panel: when on, all AI play cEDH (deck pools restricted to bracket 5) and per-seat difficulty dropdowns are disabled with a cEDH badge while preserving each seat's remembered difficulty; when off, every opponent's difficulty is independent. `effectiveAiDifficulty` maps `cedhMode` to the engine's per-seat "CEDH" contract at both init sites, so the engine is unchanged. A persisted-store migration (v11->v12) converts existing per-seat cEDH selections to `cedhMode`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ching
The Tauri initialize_game command returned Err(e.to_string()), forcing the
tauri-adapter to substring-match the Rust Display text ("not declared cEDH" /
"limited to 4 seats") to detect a cEDH bracket violation — fragile and against
the project's typed-over-stringly-typed style. Return a serializable
CommandError enum (BracketViolation / Generic) and discriminate on `kind` in
the adapter, mirroring the WASM worker path's `cedh_bracket_violation` flag so
both transports surface the violation as a typed signal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # crates/engine/tests/integration/anaphoric_scope_allowlist_guard.rs
The docs/superpowers/ plan + spec files are Claude Code plugin output and do not belong in the repo (per pr-contribution-handler skill). Removing them from the PR per maintainer confirmation.
…dant anyCedh alias
Architecture-review findings:
- GamePage cEDH bracket-violation modal (title/body/return button/aria-label)
and GameSetupPage cEDH warning chip rendered hardcoded English instead of
t(). Added gameSetup.bracketViolation.* + gameSetup.cedhWarning{,Untagged}
keys to all 7 locale game.json catalogs (real translations) and routed the
frontend-authored chrome through t(). Engine-authored error text
(bracketViolationError) intentionally stays un-t()'d per the i18n boundary.
- Removed the redundant 1:1 anyCedh alias of cedhMode in AiOpponentConfig
(leftover from the removed multi-seat cascade design).
Verified: pnpm type-check + eslint clean; GamePage.bracketViolation and
GameSetupPage suites pass against the real en catalog (test-setup loads real
i18n, so existing copy assertions validate the new keys).
…lity/decision-kind
Architecture-review finding (correctness): StructuralComboDetector collapsed a
ManaCost's colored pips + generic into a single count compared against the
colorless zone_eval::available_mana, so a colored combo line (Heliod+Ballista
{1}{W}, Thoracle+Consultation {1}{U}{U}{B}) was reported ReachableThisTurn
{missing_mana:0} whenever total mana sufficed REGARDLESS of color — driving a
+15 combo_progress_this_turn_bonus onto a combo the AI cannot actually cast.
Fix: consume the engine's existing color-accurate affordability primitive
engine::game::casting::can_pay_cost_after_auto_tap (auto-taps all mana sources
into a simulated pool, runs the real can_pay_for_spell — colored pips, generic,
hybrid, Phyrexian, {C}, restrictions, summoning sickness CR 302.6). The
cost-bearing source is derived generically from the line's first ComboStep
(Activate -> battlefield obj, Cast -> hand obj); NoCost/SelfManaCost short-circuit
(Kiki). An all-pieces-present-but-unaffordable line collapses to NotReachable, so
reachable_lines stops surfacing uncastable lines to the policy. CR 601.2g/601.2h
annotated (grep-verified).
Also (cleanup): removed dead ComboReachability::ReachableSoon (never constructed,
YAGNI) and dropped DecisionKind::SelectTarget from ComboLinePolicy::decision_kinds
(verdict can never score a target candidate -> wasted per-candidate reachable_lines
scan).
Tests: added discriminating negative regression
thoracle_line_not_reachable_with_wrong_color_mana (4 wrong-color lands -> NOT
reachable; fails under old count-based code, passes under fix); updated the 3
existing fixtures to give test lands real basic-land subtypes so the engine
auto-tap path produces the correct colors.
Reviewed-clean via engine-implementer pipeline (2 plan-review rounds, 1 impl
review). Verified with direct cargo (Tilt watches main, not this worktree):
cargo clippy -p engine -p phase-ai --all-targets clean; cargo test -p phase-ai
--lib 676 passed.
…literals Merge-interaction fix surfaced by bringing the branch current with origin/main: c71d2da (Whitemane Lion, #1268) added crates/phase-ai/tests/whitemane_lion_bounded.rs constructing DeckList/PlayerDeckList with the old field set, while this PR added bracket_tier (PlayerDeckList) and ai_difficulties (DeckList). #[serde(default)] covers deserialization but not struct-literal construction, so the test target failed to compile after the merge. Added the fields via the PR's established Default::default()/Vec::new() pattern (sibling coverage for the struct change). cargo clippy -p phase-ai --all-targets now clean.
…, collapsible groups - ManaCostPips: flex-center the glyph in its pip circle (inline-block baseline pushed it ~5px low); normalize lg padding now that the baseline hack is moot. - aiDeckCatalog: resolve a Commander candidate's bracket from the engine's computed BracketEstimate when no explicit tag exists, so the AI bracket filter stops collapsing every selection to 'Random (0)'. Explicit tags win; non-Commander formats skip estimation. Adds fallback + tag-precedence tests. - test-setup: register a lean English-only i18next instead of importing the app i18n module, which eager-loaded 49 catalogs + a store subscription per test file under worker isolation (~54% less per-file setup time). - board: make expanded permanent groups collapsible via a count badge that toggles the group (BattlefieldRow toggle + GroupedPermanent button); add the collapseGroup key across all 7 locales to satisfy the key-parity gate.
# Conflicts: # client/src/services/__tests__/aiDeckCatalog.test.ts
There was a problem hiding this comment.
Code Review
This pull request introduces table-wide cEDH mode support, featuring a dedicated cEDH toggle, hand-curated bundled cEDH decks, and a typed bracket-violation error when non-cEDH decks are brought to a cEDH table. It also adds a cEDH combo-recognition layer with a ComboLinePolicy to boost actions progressing reachable combos, alongside a CedhKeepablesMulligan policy for aggressive mulligans. Additionally, a collapse button is added to grouped permanents. Feedback highlights a critical compilation failure due to a missing invalidate_player_features method on AiSession. It also recommends gating the cEDH mode and toggle to the Commander format to prevent initialization failures, and expanding the touch target of the new collapse button to at least 44px for mobile friendliness.
I am having trouble creating individual review comments. Click here to see my feedback.
crates/phase-ai/src/session.rs (107)
[CRITICAL] Missing invalidate_player_features method on AiSession causes a compilation failure because it is called in crates/phase-ai/src/search.rs to refresh features when the AI's bracket tier is not Core.
pub fn invalidate_player_features(&mut self, player: PlayerId) {
self.features.remove(&player);
self.plan.remove(&player);
}
pub fn ensure_player_features(
client/src/pages/GameSetupPage.tsx (167)
[HIGH] Gating cedhMode on selectedFormat === "Commander" is necessary to prevent game-breaking initialization failures when playing non-Commander formats if cedhMode was left enabled in the persisted preferences.
const cedhMode = prefs.cedhMode && selectedFormat === "Commander";
client/src/providers/GameProvider.tsx (312-314)
[HIGH] Gating cedhMode on formatConfig?.format === "Commander" in buildLocalAiDeckList is necessary to prevent game-breaking initialization failures when playing non-Commander formats if cedhMode was left enabled in the persisted preferences.
const effectiveCedhMode = cedhMode && formatConfig?.format === "Commander";
const aiDifficulties = picks.map((_, i) =>
effectiveAiDifficulty(aiSeats[i]?.difficulty ?? "Medium", effectiveCedhMode),
);
client/src/components/board/GroupedPermanent.tsx (180)
[MEDIUM] The collapse button has a size of h-5 w-5 (20px), which violates the style guide rule requiring touch targets to be at least 44pt/px for mobile/touch friendliness. We can expand the touch target using a pseudo-element without changing the visual size.
className="absolute left-1 top-1 z-30 flex h-5 w-5 items-center justify-center rounded-full bg-black/80 text-[10px] font-bold text-white ring-1 ring-gray-500 transition-colors hover:bg-black before:absolute before:-inset-3"
References
- Mobile / touch: touch targets ≥ 44pt; :hover-only state breaks on mobile and needs a touch-equivalent. (link)
client/src/components/menu/AiOpponentConfig.tsx (140-161)
[MEDIUM] The table-wide cEDH toggle should only be rendered when selectedFormat === "Commander" to avoid confusing users when playing non-Commander formats.
{selectedFormat === "Commander" && (
<div className="flex items-center justify-between gap-3 rounded-lg border border-rose-500/25 bg-rose-500/5 px-3 py-2">
<div className="flex min-w-0 flex-col">
<span className="text-xs font-semibold text-rose-200">{t("aiOpponent.cedhToggle.label")}</span>
<span className="text-[10px] text-slate-400">{t("aiOpponent.cedhToggle.hint")}</span>
</div>
<button
type="button"
role="switch"
aria-checked={cedhMode}
aria-label={t("aiOpponent.cedhToggle.label")}
onClick={() => setCedhMode(!cedhMode)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-300/60 ${
cedhMode ? "bg-rose-500" : "bg-white/15"
}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
cedhMode ? "translate-x-5" : "translate-x-0.5"
}`}
/>
</button>
</div>
)}
|
Triage of the Gemini review — note this PR's net diff over
|
Net change over
mainis the 13 files below (the prior cEDH commits on this branch already landed onmainvia squash).ManaCostPips.tsx): flex-center the glyph in its circle — theinline-blockbaseline pushed it ~5px low (spilling past the bottom). Normalizelgpadding now that the baseline-compensation hack is moot.aiDeckCatalog.ts+ test): resolve a Commander candidate's bracket from the engine's computedBracketEstimatewhen no explicit tag exists, so the filter stops collapsing every selection to "Random (0)". Explicit tags win; non-Commander formats skip estimation.test-setup.ts): register a lean English-only i18next instead of importing the app i18n module (which eager-loaded 49 catalogs + a store subscription per test file under worker isolation) — ~54% less per-file setup time.BattlefieldRow.tsx,GroupedPermanent.tsx, 7× localegame.json): a count badge toggles an expanded group back to stacked;collapseGroupadded across all 7 locales to satisfy the key-parity gate.