T08 iter 5: the DARVO sequence machine
Resolution step 3 (GR-R06), the last unimplemented step: - GR-D02 binding: one stage per consecutive round, DENY then ATTACK then REVERSE, regardless of the player's later Stress. - GR-D03 DENY: turn one face-up, unsolved, unprotected Problem face down and Deny it. Under the U3 default no legal target is a no-op and the sequence still advances. - GR-D04 ATTACK: one extra Attack under the normal relation rules, then place the Focus token beside the target even if it was cancelled. - GR-D05 REVERSE: flip Focus to Blame, +1 Stress to the holder, one Protection to the owner, unless the holder's GROUND-ND rejects it. Under the U5 default the owner takes -2 either way and the sequence ends. - GR-D06 early end: a Support through a Bond that predates this round's Support step cancels the stage and ends the sequence, and the placed Focus token is removed. GROUND-GR ends it after the stage resolves. - GR-D07: the marker returns to OFF, so a later End can re-trigger. The Attack rules are now one routine shared by the chosen ATTACK Action and the DARVO extra Attack, so GR-A06..A09 cannot drift between them. Stage targets are named by their own command during Reveal, validated against what the stage admits: DENY needs an eligible Problem, ATTACK another player, and only a player with a live sequence may choose. 18 scenarios pass; AM-1 coverage 47/58 (81%), up from 41/58. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
290ad06c31
commit
0e92535f11
5 changed files with 513 additions and 61 deletions
|
|
@ -138,11 +138,22 @@ pub struct GroundState {
|
|||
pub ground_choices: BTreeMap<PlayerId, GroundChoice>,
|
||||
/// GR-L02/A05: a Support target's response, keyed by target.
|
||||
pub support_responses: BTreeMap<PlayerId, SupportResponse>,
|
||||
/// GR-D03/D04: the mandatory target a DARVO stage needs this round.
|
||||
pub darvo_targets: BTreeMap<PlayerId, DarvoTarget>,
|
||||
/// GR-S04/U4: retained so a deck reshuffle stays a pure function of
|
||||
/// state, keeping `validate` deterministic without holding RNG state.
|
||||
pub seed: u64,
|
||||
}
|
||||
|
||||
/// GR-D03/D04: what a DARVO stage acts on this round. DENY names a
|
||||
/// Problem, ATTACK names a player; REVERSE takes its target from the
|
||||
/// Focus token placed by the ATTACK stage.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DarvoTarget {
|
||||
pub problem: Option<u32>,
|
||||
pub player: Option<PlayerId>,
|
||||
}
|
||||
|
||||
/// GR-A10..A12: the three GROUND modes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GroundMode {
|
||||
|
|
@ -326,6 +337,8 @@ pub enum GroundCommand {
|
|||
},
|
||||
/// GR-L02/A05: respond to a Support aimed at this player.
|
||||
RespondToSupport { response: SupportResponse },
|
||||
/// GR-D03/D04: name the mandatory target of this round's stage.
|
||||
ChooseDarvoTarget { target: DarvoTarget },
|
||||
/// GR-R04: reveal all selections simultaneously. System-driven.
|
||||
Reveal,
|
||||
/// GR-R06/R07: resolve revealed Actions in fixed step order, Lead
|
||||
|
|
@ -425,6 +438,38 @@ pub enum GroundEvent {
|
|||
DarvoTriggered {
|
||||
player: PlayerId,
|
||||
},
|
||||
/// GR-D03/D04.
|
||||
DarvoTargetChosen {
|
||||
player: PlayerId,
|
||||
target: DarvoTarget,
|
||||
},
|
||||
/// GR-D03: a Problem was turned face down and Denied.
|
||||
ProblemDenied {
|
||||
problem: u32,
|
||||
},
|
||||
/// GR-D04/T03: the sequence owner's Focus token was placed.
|
||||
FocusPlaced {
|
||||
owner: PlayerId,
|
||||
target: PlayerId,
|
||||
},
|
||||
/// GR-D05/T03: Focus flipped to Blame in front of the target.
|
||||
FocusFlippedToBlame {
|
||||
owner: PlayerId,
|
||||
target: PlayerId,
|
||||
},
|
||||
/// GR-D05/T01.
|
||||
ProtectionGained {
|
||||
player: PlayerId,
|
||||
},
|
||||
/// GR-D02: the sequence moved to its next stage.
|
||||
DarvoAdvanced {
|
||||
player: PlayerId,
|
||||
stage: DarvoStage,
|
||||
},
|
||||
/// GR-D05/D06/D07: the sequence ended and the marker is OFF again.
|
||||
DarvoEnded {
|
||||
player: PlayerId,
|
||||
},
|
||||
/// GR-R01: the round advanced to `step`.
|
||||
StepAdvanced {
|
||||
step: RoundStep,
|
||||
|
|
@ -679,6 +724,64 @@ impl Aggregate for GroundState {
|
|||
response: *response,
|
||||
}])
|
||||
}
|
||||
// GR-D03/D04: only a player with a live sequence, after
|
||||
// Reveal, once per round.
|
||||
GroundCommand::ChooseDarvoTarget { target } => {
|
||||
if self.step != RoundStep::Reveal {
|
||||
return Err(Rejection::NotAllowedNow);
|
||||
}
|
||||
if player.darvo == DarvoStage::Off {
|
||||
return Err(Rejection::Game {
|
||||
code: "no-darvo-sequence".into(),
|
||||
detail: "GR-D02: this player has no live DARVO sequence".into(),
|
||||
});
|
||||
}
|
||||
if self.darvo_targets.contains_key(&id) {
|
||||
return Err(Rejection::DuplicateCommand);
|
||||
}
|
||||
match player.darvo {
|
||||
// GR-D03: a face-up, unsolved, unprotected Problem.
|
||||
DarvoStage::Deny => {
|
||||
let problem = target.problem.ok_or(Rejection::Game {
|
||||
code: "bad-darvo-target".into(),
|
||||
detail: "GR-D03: DENY names a Problem".into(),
|
||||
})?;
|
||||
let eligible = self.problems.get(&problem).is_some_and(|p| {
|
||||
p.face_up
|
||||
&& !p.denied
|
||||
&& p.claimed_by.is_none()
|
||||
&& !p.protected_this_round
|
||||
});
|
||||
if !eligible {
|
||||
return Err(Rejection::Game {
|
||||
code: "bad-darvo-target".into(),
|
||||
detail: format!(
|
||||
"GR-D03: Problem {problem} is not face-up, unsolved and unprotected"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
// GR-D04: an extra Attack against another player.
|
||||
DarvoStage::Attack => {
|
||||
let other = target.player.ok_or(Rejection::Game {
|
||||
code: "bad-darvo-target".into(),
|
||||
detail: "GR-D04: ATTACK names a player".into(),
|
||||
})?;
|
||||
if other == id || !self.players.contains_key(&other) {
|
||||
return Err(Rejection::Game {
|
||||
code: "bad-darvo-target".into(),
|
||||
detail: "GR-D04: ATTACK targets another player".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// GR-D05: REVERSE uses the placed Focus token.
|
||||
DarvoStage::Reverse | DarvoStage::Off => {}
|
||||
}
|
||||
Ok(vec![GroundEvent::DarvoTargetChosen {
|
||||
player: id,
|
||||
target: *target,
|
||||
}])
|
||||
}
|
||||
GroundCommand::Reveal | GroundCommand::Resolve | GroundCommand::EndRound => {
|
||||
unreachable!("system commands are handled above")
|
||||
}
|
||||
|
|
@ -783,6 +886,41 @@ impl Aggregate for GroundState {
|
|||
state.darvo = DarvoStage::Deny;
|
||||
}
|
||||
}
|
||||
GroundEvent::DarvoTargetChosen { player, target } => {
|
||||
self.darvo_targets.insert(*player, *target);
|
||||
}
|
||||
GroundEvent::ProblemDenied { problem } => {
|
||||
if let Some(state) = self.problems.get_mut(problem) {
|
||||
state.denied = true;
|
||||
state.face_up = false;
|
||||
}
|
||||
}
|
||||
GroundEvent::FocusPlaced { owner, target } => {
|
||||
self.focus.insert(*owner, *target);
|
||||
}
|
||||
GroundEvent::FocusFlippedToBlame { owner, target } => {
|
||||
self.focus.remove(owner);
|
||||
if let Some(state) = self.players.get_mut(target) {
|
||||
state.blame_from.push(*owner);
|
||||
}
|
||||
}
|
||||
GroundEvent::ProtectionGained { player } => {
|
||||
if let Some(state) = self.players.get_mut(player) {
|
||||
state.protection = state.protection.saturating_add(1);
|
||||
}
|
||||
}
|
||||
GroundEvent::DarvoAdvanced { player, stage } => {
|
||||
if let Some(state) = self.players.get_mut(player) {
|
||||
state.darvo = *stage;
|
||||
}
|
||||
}
|
||||
GroundEvent::DarvoEnded { player } => {
|
||||
if let Some(state) = self.players.get_mut(player) {
|
||||
state.darvo = DarvoStage::Off;
|
||||
}
|
||||
// GR-D06: an unresolved Focus token comes back.
|
||||
self.focus.remove(player);
|
||||
}
|
||||
GroundEvent::StepAdvanced { step } => {
|
||||
self.step = *step;
|
||||
}
|
||||
|
|
@ -793,6 +931,7 @@ impl Aggregate for GroundState {
|
|||
self.ground_modes.clear();
|
||||
self.ground_choices.clear();
|
||||
self.support_responses.clear();
|
||||
self.darvo_targets.clear();
|
||||
for state in self.players.values_mut() {
|
||||
// GR-R03: the gate lift lasts one Select step only.
|
||||
state.freedom_gate_lifted = false;
|
||||
|
|
@ -958,6 +1097,106 @@ impl GroundState {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 3 — active DARVO stages (GR-D02..D07).
|
||||
for owner in self.seat_order() {
|
||||
let stage = work.players[&owner].darvo;
|
||||
if stage == DarvoStage::Off {
|
||||
continue;
|
||||
}
|
||||
|
||||
// GR-D06 + GR-A04: a Support through a Bond that existed
|
||||
// before this round's Support step cancels the current stage
|
||||
// and ends the sequence (GR-L05).
|
||||
let bond_support = self.selections.iter().any(|(seat, sel)| {
|
||||
sel.action == Action::Support
|
||||
&& sel.target == Some(owner)
|
||||
&& pre_existing.get(&Pair::new(*seat, owner)) == Some(&Relation::Bond)
|
||||
});
|
||||
if bond_support {
|
||||
events.push(GroundEvent::DarvoEnded { player: owner });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
continue;
|
||||
}
|
||||
|
||||
match stage {
|
||||
// GR-D03: turn one eligible Problem face down and Deny
|
||||
// it. Under the U3 default, no legal target is a no-op
|
||||
// and the sequence still advances.
|
||||
DarvoStage::Deny => {
|
||||
if let Some(problem) = work.darvo_targets.get(&owner).and_then(|t| t.problem) {
|
||||
let eligible = work.problems.get(&problem).is_some_and(|p| {
|
||||
p.face_up
|
||||
&& !p.denied
|
||||
&& p.claimed_by.is_none()
|
||||
&& !p.protected_this_round
|
||||
});
|
||||
if eligible {
|
||||
events.push(GroundEvent::ProblemDenied { problem });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// GR-D04: one extra Attack under the normal relation
|
||||
// rules, then place the Focus token beside the target —
|
||||
// even if the Attack was cancelled.
|
||||
DarvoStage::Attack => {
|
||||
if let Some(target) = work.darvo_targets.get(&owner).and_then(|t| t.player) {
|
||||
work.resolve_attack(owner, target, &ou_cancels, &mut events);
|
||||
events.push(GroundEvent::FocusPlaced { owner, target });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
// GR-D05: targets the Focus holder.
|
||||
DarvoStage::Reverse => {
|
||||
if let Some(target) = work.focus.get(&owner).copied() {
|
||||
// GR-A12: the target's GROUND—ND may reject it.
|
||||
let rejected =
|
||||
work.ground_choices.get(&target) == Some(&GroundChoice::RejectReverse);
|
||||
if !rejected {
|
||||
events.push(GroundEvent::FocusFlippedToBlame { owner, target });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
let stress = work.stress_after(target, 1);
|
||||
events.push(GroundEvent::StressSet {
|
||||
player: target,
|
||||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
events.push(GroundEvent::ProtectionGained { player: owner });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
// U5: rejected or not, the owner still takes −2
|
||||
// and the sequence ends.
|
||||
let stress = work.stress_after(owner, -2);
|
||||
events.push(GroundEvent::StressSet {
|
||||
player: owner,
|
||||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
DarvoStage::Off => {}
|
||||
}
|
||||
|
||||
// GR-A10 + GR-D06: GROUND—GR ends the sequence after the
|
||||
// current stage resolves. GR-D05: REVERSE ends it anyway.
|
||||
let ground_gr = work.ground_modes.get(&owner) == Some(&GroundMode::Gr);
|
||||
if ground_gr || stage == DarvoStage::Reverse {
|
||||
events.push(GroundEvent::DarvoEnded { player: owner });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
} else {
|
||||
// GR-D02: one stage per consecutive round.
|
||||
let next = match stage {
|
||||
DarvoStage::Deny => DarvoStage::Attack,
|
||||
_ => DarvoStage::Reverse,
|
||||
};
|
||||
events.push(GroundEvent::DarvoAdvanced {
|
||||
player: owner,
|
||||
stage: next,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5 — Attack (GR-A06..A09).
|
||||
for actor in self.seat_order() {
|
||||
let Some(selection) = self.selections.get(&actor) else {
|
||||
|
|
@ -970,67 +1209,7 @@ impl GroundState {
|
|||
continue;
|
||||
};
|
||||
|
||||
// GR-A09 under the U8 default: a GROUND—OU cancellation is
|
||||
// chosen at step 1 and applies first, so Protection is only
|
||||
// consumed when it is what actually cancels.
|
||||
if ou_cancels.contains(&(actor, target)) {
|
||||
continue;
|
||||
}
|
||||
// GR-A09/T01: Protection absorbs the Attack entirely.
|
||||
if work.players[&target].protection > 0 {
|
||||
events.push(GroundEvent::AttackCancelled {
|
||||
attacker: actor,
|
||||
target,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
continue;
|
||||
}
|
||||
|
||||
let pair = Pair::new(actor, target);
|
||||
match work.relation_between(actor, target) {
|
||||
// GR-A07: Attack through a Bond — +2 and the Bond flips.
|
||||
Some(Relation::Bond) => {
|
||||
let stress = work.stress_after(target, 2);
|
||||
events.push(GroundEvent::StressSet {
|
||||
player: target,
|
||||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
events.push(GroundEvent::RelationFormed {
|
||||
pair,
|
||||
relation: Relation::Rivalry,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
// GR-A08: Attack through a Rivalry — +2 and it breaks.
|
||||
Some(Relation::Rivalry) => {
|
||||
let stress = work.stress_after(target, 2);
|
||||
events.push(GroundEvent::StressSet {
|
||||
player: target,
|
||||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
events.push(GroundEvent::RelationBroken { pair });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
// GR-A06: no relation — +1, and a Rivalry forms without
|
||||
// consent if both endpoints have a slot (GR-L01/L03).
|
||||
None => {
|
||||
let stress = work.stress_after(target, 1);
|
||||
events.push(GroundEvent::StressSet {
|
||||
player: target,
|
||||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
if work.has_free_slot(actor) && work.has_free_slot(target) {
|
||||
events.push(GroundEvent::RelationFormed {
|
||||
pair,
|
||||
relation: Relation::Rivalry,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
work.resolve_attack(actor, target, &ou_cancels, &mut events);
|
||||
}
|
||||
|
||||
// Step 4 — INVESTIGATE (GR-A01).
|
||||
|
|
@ -1099,6 +1278,67 @@ impl GroundState {
|
|||
events
|
||||
}
|
||||
|
||||
/// GR-A06..A09: one Attack under the normal relation rules. Shared
|
||||
/// by the chosen ATTACK Action (step 5) and the DARVO ATTACK stage's
|
||||
/// extra Attack (GR-D04).
|
||||
fn resolve_attack(
|
||||
&mut self,
|
||||
attacker: PlayerId,
|
||||
target: PlayerId,
|
||||
ou_cancels: &std::collections::BTreeSet<(PlayerId, PlayerId)>,
|
||||
events: &mut Vec<GroundEvent>,
|
||||
) {
|
||||
// GR-A09 under the U8 default: a GROUND—OU cancellation is
|
||||
// chosen at step 1 and applies first, so Protection is only
|
||||
// consumed when it is what actually cancels.
|
||||
if ou_cancels.contains(&(attacker, target)) {
|
||||
return;
|
||||
}
|
||||
// GR-A09/T01: Protection absorbs the Attack entirely.
|
||||
if self.players[&target].protection > 0 {
|
||||
events.push(GroundEvent::AttackCancelled { attacker, target });
|
||||
self.fold(events.last().expect("just pushed"));
|
||||
return;
|
||||
}
|
||||
|
||||
let pair = Pair::new(attacker, target);
|
||||
let (delta, after) = match self.relation_between(attacker, target) {
|
||||
// GR-A07: through a Bond — +2 and the Bond flips.
|
||||
Some(Relation::Bond) => (
|
||||
2,
|
||||
Some(GroundEvent::RelationFormed {
|
||||
pair,
|
||||
relation: Relation::Rivalry,
|
||||
}),
|
||||
),
|
||||
// GR-A08: through a Rivalry — +2 and it breaks.
|
||||
Some(Relation::Rivalry) => (2, Some(GroundEvent::RelationBroken { pair })),
|
||||
// GR-A06: no relation — +1, and a Rivalry forms without
|
||||
// consent if both endpoints have a slot (GR-L01/L03).
|
||||
None => {
|
||||
let forms = self.has_free_slot(attacker) && self.has_free_slot(target);
|
||||
(
|
||||
1,
|
||||
forms.then_some(GroundEvent::RelationFormed {
|
||||
pair,
|
||||
relation: Relation::Rivalry,
|
||||
}),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let stress = self.stress_after(target, delta);
|
||||
events.push(GroundEvent::StressSet {
|
||||
player: target,
|
||||
stress,
|
||||
});
|
||||
self.fold(events.last().expect("just pushed"));
|
||||
if let Some(event) = after {
|
||||
events.push(event);
|
||||
self.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
|
||||
/// GR-A01: draw one Solution, reshuffling the discard first if the
|
||||
/// deck is empty (the U4 default). The reshuffled order travels in
|
||||
/// the event so replay never re-derives it.
|
||||
|
|
@ -1367,6 +1607,7 @@ impl ScenarioGame for GroundState {
|
|||
ground_modes: BTreeMap::new(),
|
||||
ground_choices: BTreeMap::new(),
|
||||
support_responses: BTreeMap::new(),
|
||||
darvo_targets: BTreeMap::new(),
|
||||
seed,
|
||||
})
|
||||
}
|
||||
|
|
@ -1408,6 +1649,18 @@ impl ScenarioGame for GroundState {
|
|||
None => None,
|
||||
},
|
||||
},
|
||||
"choose_darvo_target" => GroundCommand::ChooseDarvoTarget {
|
||||
target: DarvoTarget {
|
||||
problem: arg_u64("problem").map(|p| p as u32),
|
||||
player: match step.args.get("target") {
|
||||
Some(_) => match parse_actor(&arg_str("target")?)? {
|
||||
Actor::Player(seat) => Some(seat),
|
||||
Actor::System => return Err("target may not be SYSTEM".into()),
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
},
|
||||
},
|
||||
"respond_to_support" => GroundCommand::RespondToSupport {
|
||||
response: SupportResponse::parse(&arg_str("response")?)?,
|
||||
},
|
||||
|
|
@ -1455,6 +1708,7 @@ mod tests {
|
|||
ground_modes: BTreeMap::new(),
|
||||
ground_choices: BTreeMap::new(),
|
||||
support_responses: BTreeMap::new(),
|
||||
darvo_targets: BTreeMap::new(),
|
||||
seed: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
scenarios/ground/gr-d03-darvo-deny.yaml
Normal file
48
scenarios/ground/gr-d03-darvo-deny.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
scenario: ground/gr-d03-darvo-deny
|
||||
description: >
|
||||
The DENY stage turns one face-up, unsolved, unprotected Problem face
|
||||
down and Denies it (GR-D03), then the sequence advances to ATTACK
|
||||
(GR-D02). A protected Problem is not a legal DENY target (GR-A11).
|
||||
covers: [GR-D02, GR-D03, GR-P01]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"players.0.darvo": Deny
|
||||
"problems.2.face_up": true
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
# GR-D02: only a player with a live sequence names a stage target.
|
||||
- actor: P2
|
||||
cmd: choose_darvo_target
|
||||
args: { problem: 1 }
|
||||
- actor: P1
|
||||
cmd: choose_darvo_target
|
||||
args: { problem: 1 }
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: ProblemDenied
|
||||
problem: 1
|
||||
- kind: DarvoAdvanced
|
||||
player: 0
|
||||
stage: Attack
|
||||
state:
|
||||
"problems.1.denied": true
|
||||
"problems.1.face_up": false
|
||||
"players.0.darvo": Attack
|
||||
rejects: [4]
|
||||
53
scenarios/ground/gr-d04-darvo-attack.yaml
Normal file
53
scenarios/ground/gr-d04-darvo-attack.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
scenario: ground/gr-d04-darvo-attack
|
||||
description: >
|
||||
The ATTACK stage makes one extra Attack under the normal relation
|
||||
rules and places the Focus token beside the target (GR-D04), then
|
||||
advances to REVERSE.
|
||||
covers: [GR-D04, GR-T03]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"players.0.darvo": Attack
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
# GR-D04: the extra Attack targets another player.
|
||||
- actor: P1
|
||||
cmd: choose_darvo_target
|
||||
args: { target: P1 }
|
||||
- actor: P1
|
||||
cmd: choose_darvo_target
|
||||
args: { target: P2 }
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: StressSet
|
||||
player: 1
|
||||
stress: 3
|
||||
- kind: FocusPlaced
|
||||
owner: 0
|
||||
target: 1
|
||||
- kind: DarvoAdvanced
|
||||
player: 0
|
||||
stage: Reverse
|
||||
state:
|
||||
"players.1.stress": 3
|
||||
"focus.0": 1
|
||||
"players.0.darvo": Reverse
|
||||
# GR-L03: the extra Attack forms a Rivalry like any other.
|
||||
"relations.0-1": Rivalry
|
||||
rejects: [4]
|
||||
54
scenarios/ground/gr-d05-darvo-reverse.yaml
Normal file
54
scenarios/ground/gr-d05-darvo-reverse.yaml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
scenario: ground/gr-d05-darvo-reverse
|
||||
description: >
|
||||
The REVERSE stage flips Focus to Blame in front of the Focus holder,
|
||||
gives them +1 Stress and the owner one Protection token, then the
|
||||
owner takes −2 Stress and the sequence ends (GR-D05, GR-D07).
|
||||
covers: [GR-D05, GR-D07, GR-T01]
|
||||
provisional: true
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"players.0.darvo": Reverse
|
||||
"players.0.stress": 3
|
||||
"focus.0": 1
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: FocusFlippedToBlame
|
||||
owner: 0
|
||||
target: 1
|
||||
- kind: StressSet
|
||||
player: 1
|
||||
stress: 3
|
||||
- kind: ProtectionGained
|
||||
player: 0
|
||||
- kind: StressSet
|
||||
player: 0
|
||||
stress: 1
|
||||
- kind: DarvoEnded
|
||||
player: 0
|
||||
state:
|
||||
"players.1.blame_from": [0]
|
||||
"players.1.stress": 3
|
||||
"players.0.protection": 1
|
||||
"players.0.stress": 1
|
||||
# GR-D07: the marker is OFF, so a new sequence can trigger later.
|
||||
"players.0.darvo": Off
|
||||
"focus": {}
|
||||
rejects: []
|
||||
43
scenarios/ground/gr-d06-darvo-early-end.yaml
Normal file
43
scenarios/ground/gr-d06-darvo-early-end.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
scenario: ground/gr-d06-darvo-early-end
|
||||
description: >
|
||||
A Support through a Bond that existed before this round cancels the
|
||||
current stage and ends the sequence; the placed Focus token is removed
|
||||
(GR-D06, GR-A04, GR-L05).
|
||||
covers: [GR-D06, GR-L05]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"players.0.darvo": Attack
|
||||
"focus.0": 2
|
||||
"relations.0-1": Bond
|
||||
commands:
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args: { action: SUPPORT, target: P1 }
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
- actor: P1
|
||||
cmd: choose_darvo_target
|
||||
args: { target: P3 }
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: DarvoEnded
|
||||
player: 0
|
||||
state:
|
||||
"players.0.darvo": Off
|
||||
# GR-D06: the stage never fired, so P3 took no extra Attack.
|
||||
"players.2.stress": 2
|
||||
"focus": {}
|
||||
rejects: []
|
||||
Loading…
Add table
Add a link
Reference in a new issue