Card(s)
Corpseberry Cultivator. Any "Whenever you forage" card is affected.
Build/version
phase-engine 0.44.0, base 1bb6c1d.
Game mode
P2P
Actual behavior
Foraging never fires "Whenever you forage" — Corpseberry Cultivator foraged at the beginning of combat (sacrificing a Food) and got no +1/+1 counter.
The engine declares the gap itself; from the game-state dump:
"unimplemented_mechanics": ["Trigger: Whenever you forage"]
The effect parses correctly (PutCounter{ P1P1, SelfRef }); only the triggering condition is lost, as mode: {"Unknown": "Whenever you forage"}.
Expected behavior
CR 701.61a: completing a forage fires the trigger and adds the counter.
Root cause
Same shape as #4042 ("Whenever you investigate" → TriggerMode::Unknown), and broken on both ends of the shared player-action seam:
parse_player_action_phrase (parser/oracle_trigger.rs) has arms for scry, surveil, collect evidence, investigate, … but none for forage, so the trigger lands as TriggerMode::Unknown and trigger_matcher returns None.
effects::forage emits only EffectResolved{ kind: Forage }, which carries no acting player and so cannot serve a player-scoped trigger. Its siblings effects::surveil / investigate / proliferate each announce GameEvent::PlayerPerformedAction; forage does not, and PlayerActionKind has no Forage variant.
So fixing either end alone changes nothing.
Note TriggerMode::Forage exists and looks wired, but is a dead end: trigger_matcher maps it into the arm ending => match_visit_attraction, which matches only AttractionVisited. Implementing against it would be the wrong seam — proliferate, the closest analogue, routes through PlayerPerformedAction instead.
Suggested fix
Three additions, mirroring proliferate exactly. Verified locally: cargo test --test integration 4804 passed | 0 failed, cargo test -p phase-engine --lib 18832 passed | 0 failed, clippy clean.
// types/events.rs — PlayerActionKind
/// CR 701.61a: A player foraged (exiled three cards from their graveyard,
/// or sacrificed a Food).
Forage,
// parser/oracle_trigger.rs — parse_player_action_phrase, beside "surveil"
"forage" | "forages" => Some(PlayerActionKind::Forage),
// game/effects/forage.rs — resolve(), after the branch match
// Gated: with neither three graveyard cards nor a Food, no forage occurred
// (CR 701.61a), so the trigger must not fire.
let foraged = !branches.is_empty(); // captured before the match consumes `branches`
if foraged {
events.push(GameEvent::PlayerPerformedAction {
player_id: controller,
action: PlayerActionKind::Forage,
look_count: None,
scry_bottom_count: None,
scry_top_count: None,
});
}
Steps to reproduce
Battlefield: Corpseberry Cultivator, three cards in your graveyard. Accept the begin-combat forage, exile three cards — no counter appears.
As a test (crates/engine/tests/integration/), driving the real prompt chain:
const CORPSEBERRY_CULTIVATOR: &str = "At the beginning of combat on your turn, you may forage. \
(Exile three cards from your graveyard or sacrifice a Food.)\n\
Whenever you forage, put a +1/+1 counter on this creature.";
#[test]
fn foraging_puts_a_counter_on_the_cultivator() {
let mut scenario = GameScenario::new();
scenario.at_phase(Phase::PreCombatMain);
let cultivator = scenario
.add_creature_from_oracle(P0, "Corpseberry Cultivator", 2, 3, CORPSEBERRY_CULTIVATOR)
.id();
for _ in 0..3 {
scenario.add_creature_to_graveyard(P0, "Fodder", 1, 1);
}
let mut runner = scenario.build();
for _ in 0..200 {
if p1p1(&runner, cultivator) > 0 {
break;
}
match &runner.state().waiting_for {
WaitingFor::OptionalEffectChoice { .. } => {
runner.act(GameAction::DecideOptionalEffect { accept: true }).unwrap();
}
WaitingFor::EffectZoneChoice { cards, count, .. } => {
let pick: Vec<_> = cards.iter().take(*count).copied().collect();
runner.act(GameAction::SelectCards { cards: pick }).unwrap();
}
_ => {
if runner.act(GameAction::PassPriority).is_err() {
break;
}
}
}
}
assert_eq!(p1p1(&runner, cultivator), 1);
}
Two-sided controls, each failing its own half: dropping the parser arm fails the parse, the not-unimplemented and the counter assertions; dropping the PlayerPerformedAction emission fails only the counter assertion.
Logs/screenshots/game-state
Turn-13 state dump available on request; the relevant extract is quoted above.
Diagnosed with an LLM (claude-opus-5). No PR opened: CONTRIBUTING.md routes crates/engine/ changes through /engine-implementer, which this session did not run.
Card(s)
Corpseberry Cultivator. Any "Whenever you forage" card is affected.
Build/version
phase-engine0.44.0, base1bb6c1d.Game mode
P2P
Actual behavior
Foraging never fires "Whenever you forage" — Corpseberry Cultivator foraged at the beginning of combat (sacrificing a Food) and got no +1/+1 counter.
The engine declares the gap itself; from the game-state dump:
The effect parses correctly (
PutCounter{ P1P1, SelfRef }); only the triggering condition is lost, asmode: {"Unknown": "Whenever you forage"}.Expected behavior
CR 701.61a: completing a forage fires the trigger and adds the counter.
Root cause
Same shape as #4042 ("Whenever you investigate" →
TriggerMode::Unknown), and broken on both ends of the shared player-action seam:parse_player_action_phrase(parser/oracle_trigger.rs) has arms forscry,surveil,collect evidence,investigate, … but none forforage, so the trigger lands asTriggerMode::Unknownandtrigger_matcherreturnsNone.effects::forageemits onlyEffectResolved{ kind: Forage }, which carries no acting player and so cannot serve a player-scoped trigger. Its siblingseffects::surveil/investigate/proliferateeach announceGameEvent::PlayerPerformedAction; forage does not, andPlayerActionKindhas noForagevariant.So fixing either end alone changes nothing.
Note
TriggerMode::Forageexists and looks wired, but is a dead end:trigger_matchermaps it into the arm ending=> match_visit_attraction, which matches onlyAttractionVisited. Implementing against it would be the wrong seam — proliferate, the closest analogue, routes throughPlayerPerformedActioninstead.Suggested fix
Three additions, mirroring proliferate exactly. Verified locally:
cargo test --test integration4804 passed | 0 failed,cargo test -p phase-engine --lib18832 passed | 0 failed, clippy clean.Steps to reproduce
Battlefield: Corpseberry Cultivator, three cards in your graveyard. Accept the begin-combat forage, exile three cards — no counter appears.
As a test (
crates/engine/tests/integration/), driving the real prompt chain:Two-sided controls, each failing its own half: dropping the parser arm fails the parse, the not-unimplemented and the counter assertions; dropping the
PlayerPerformedActionemission fails only the counter assertion.Logs/screenshots/game-state
Turn-13 state dump available on request; the relevant extract is quoted above.
Diagnosed with an LLM (claude-opus-5). No PR opened:
CONTRIBUTING.mdroutescrates/engine/changes through/engine-implementer, which this session did not run.