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>,
|
pub ground_choices: BTreeMap<PlayerId, GroundChoice>,
|
||||||
/// GR-L02/A05: a Support target's response, keyed by target.
|
/// GR-L02/A05: a Support target's response, keyed by target.
|
||||||
pub support_responses: BTreeMap<PlayerId, SupportResponse>,
|
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
|
/// GR-S04/U4: retained so a deck reshuffle stays a pure function of
|
||||||
/// state, keeping `validate` deterministic without holding RNG state.
|
/// state, keeping `validate` deterministic without holding RNG state.
|
||||||
pub seed: u64,
|
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.
|
/// GR-A10..A12: the three GROUND modes.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum GroundMode {
|
pub enum GroundMode {
|
||||||
|
|
@ -326,6 +337,8 @@ pub enum GroundCommand {
|
||||||
},
|
},
|
||||||
/// GR-L02/A05: respond to a Support aimed at this player.
|
/// GR-L02/A05: respond to a Support aimed at this player.
|
||||||
RespondToSupport { response: SupportResponse },
|
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.
|
/// GR-R04: reveal all selections simultaneously. System-driven.
|
||||||
Reveal,
|
Reveal,
|
||||||
/// GR-R06/R07: resolve revealed Actions in fixed step order, Lead
|
/// GR-R06/R07: resolve revealed Actions in fixed step order, Lead
|
||||||
|
|
@ -425,6 +438,38 @@ pub enum GroundEvent {
|
||||||
DarvoTriggered {
|
DarvoTriggered {
|
||||||
player: PlayerId,
|
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`.
|
/// GR-R01: the round advanced to `step`.
|
||||||
StepAdvanced {
|
StepAdvanced {
|
||||||
step: RoundStep,
|
step: RoundStep,
|
||||||
|
|
@ -679,6 +724,64 @@ impl Aggregate for GroundState {
|
||||||
response: *response,
|
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 => {
|
GroundCommand::Reveal | GroundCommand::Resolve | GroundCommand::EndRound => {
|
||||||
unreachable!("system commands are handled above")
|
unreachable!("system commands are handled above")
|
||||||
}
|
}
|
||||||
|
|
@ -783,6 +886,41 @@ impl Aggregate for GroundState {
|
||||||
state.darvo = DarvoStage::Deny;
|
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 } => {
|
GroundEvent::StepAdvanced { step } => {
|
||||||
self.step = *step;
|
self.step = *step;
|
||||||
}
|
}
|
||||||
|
|
@ -793,6 +931,7 @@ impl Aggregate for GroundState {
|
||||||
self.ground_modes.clear();
|
self.ground_modes.clear();
|
||||||
self.ground_choices.clear();
|
self.ground_choices.clear();
|
||||||
self.support_responses.clear();
|
self.support_responses.clear();
|
||||||
|
self.darvo_targets.clear();
|
||||||
for state in self.players.values_mut() {
|
for state in self.players.values_mut() {
|
||||||
// GR-R03: the gate lift lasts one Select step only.
|
// GR-R03: the gate lift lasts one Select step only.
|
||||||
state.freedom_gate_lifted = false;
|
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).
|
// Step 5 — Attack (GR-A06..A09).
|
||||||
for actor in self.seat_order() {
|
for actor in self.seat_order() {
|
||||||
let Some(selection) = self.selections.get(&actor) else {
|
let Some(selection) = self.selections.get(&actor) else {
|
||||||
|
|
@ -970,67 +1209,7 @@ impl GroundState {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GR-A09 under the U8 default: a GROUND—OU cancellation is
|
work.resolve_attack(actor, target, &ou_cancels, &mut events);
|
||||||
// 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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4 — INVESTIGATE (GR-A01).
|
// Step 4 — INVESTIGATE (GR-A01).
|
||||||
|
|
@ -1099,6 +1278,67 @@ impl GroundState {
|
||||||
events
|
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
|
/// GR-A01: draw one Solution, reshuffling the discard first if the
|
||||||
/// deck is empty (the U4 default). The reshuffled order travels in
|
/// deck is empty (the U4 default). The reshuffled order travels in
|
||||||
/// the event so replay never re-derives it.
|
/// the event so replay never re-derives it.
|
||||||
|
|
@ -1367,6 +1607,7 @@ impl ScenarioGame for GroundState {
|
||||||
ground_modes: BTreeMap::new(),
|
ground_modes: BTreeMap::new(),
|
||||||
ground_choices: BTreeMap::new(),
|
ground_choices: BTreeMap::new(),
|
||||||
support_responses: BTreeMap::new(),
|
support_responses: BTreeMap::new(),
|
||||||
|
darvo_targets: BTreeMap::new(),
|
||||||
seed,
|
seed,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1408,6 +1649,18 @@ impl ScenarioGame for GroundState {
|
||||||
None => None,
|
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 {
|
"respond_to_support" => GroundCommand::RespondToSupport {
|
||||||
response: SupportResponse::parse(&arg_str("response")?)?,
|
response: SupportResponse::parse(&arg_str("response")?)?,
|
||||||
},
|
},
|
||||||
|
|
@ -1455,6 +1708,7 @@ mod tests {
|
||||||
ground_modes: BTreeMap::new(),
|
ground_modes: BTreeMap::new(),
|
||||||
ground_choices: BTreeMap::new(),
|
ground_choices: BTreeMap::new(),
|
||||||
support_responses: BTreeMap::new(),
|
support_responses: BTreeMap::new(),
|
||||||
|
darvo_targets: BTreeMap::new(),
|
||||||
seed: 0,
|
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