T08 iter 4: GROUND-OU/ND choices and Support consent
Closes the three rules deferred in iter 2 and 3 because each needed a decision from a player rather than a default I would have invented: - GR-A11 GROUND-OU: restore a Denied Problem, protect a face-up one from Deny, or cancel an Attack aimed at the chooser. - GR-A12 GROUND-ND: remove a Blame token, break a relation involving the chooser, or reject a Reverse. - GR-L02 / GR-A05: the Support target answers after Reveal — accept or decline a Bond where no relation exists, flip or break an existing Rivalry. A Bond can no longer form without consent. Each choice is validated against the state it claims to act on: a restore needs a Denied Problem, a cancel needs an Attack actually aimed at the chooser, a Blame removal needs that token present, a break needs that relation to exist. GR takes no sub-choice, and OU and ND may only use choices from their own list. GR-A09 ordering follows the U8 default: an OU cancellation is chosen at step 1 and applies first, so a Protection token is spent only when it is what actually cancels. 14 scenarios pass; AM-1 coverage 41/58 (70%), up from 34/58. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
27016af216
commit
290ad06c31
5 changed files with 512 additions and 13 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
active: true
|
||||
iteration: 2
|
||||
iteration: 3
|
||||
session_id: 8cbd5701-a096-45a4-a419-9b7b1c9419bc
|
||||
max_iterations: 20
|
||||
completion_promise: "HEUREKA"
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ pub struct GroundState {
|
|||
pub selections: BTreeMap<PlayerId, Selection>,
|
||||
/// GR-R05: GROUND modes chosen after Reveal, before Resolve.
|
||||
pub ground_modes: BTreeMap<PlayerId, GroundMode>,
|
||||
/// GR-A11/A12: the sub-choice accompanying an OU or ND mode.
|
||||
pub ground_choices: BTreeMap<PlayerId, GroundChoice>,
|
||||
/// GR-L02/A05: a Support target's response, keyed by target.
|
||||
pub support_responses: BTreeMap<PlayerId, SupportResponse>,
|
||||
/// GR-S04/U4: retained so a deck reshuffle stays a pure function of
|
||||
/// state, keeping `validate` deterministic without holding RNG state.
|
||||
pub seed: u64,
|
||||
|
|
@ -150,6 +154,89 @@ pub enum GroundMode {
|
|||
Nd,
|
||||
}
|
||||
|
||||
/// GR-A11/A12: the sub-choice a GROUND—OU or GROUND—ND player makes
|
||||
/// alongside the mode. GROUND—GR takes none.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "choice")]
|
||||
pub enum GroundChoice {
|
||||
/// GR-A11: restore one Denied Problem.
|
||||
RestoreProblem { problem: u32 },
|
||||
/// GR-A11: cancel one Attack targeting this player this round.
|
||||
CancelAttack { attacker: PlayerId },
|
||||
/// GR-A11: protect one face-up Problem from Deny this round.
|
||||
ProtectProblem { problem: u32 },
|
||||
/// GR-A12: remove one Blame token from this player.
|
||||
RemoveBlame { owner: PlayerId },
|
||||
/// GR-A12: break one relation involving this player.
|
||||
BreakRelation { with: PlayerId },
|
||||
/// GR-A12: reject one Reverse targeting this player this round.
|
||||
RejectReverse,
|
||||
}
|
||||
|
||||
impl GroundChoice {
|
||||
/// GR-A11/A12: each choice belongs to exactly one mode.
|
||||
fn mode(self) -> GroundMode {
|
||||
match self {
|
||||
GroundChoice::RestoreProblem { .. }
|
||||
| GroundChoice::CancelAttack { .. }
|
||||
| GroundChoice::ProtectProblem { .. } => GroundMode::Ou,
|
||||
GroundChoice::RemoveBlame { .. }
|
||||
| GroundChoice::BreakRelation { .. }
|
||||
| GroundChoice::RejectReverse => GroundMode::Nd,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(raw: &str, arg: Option<u64>) -> Result<Self, String> {
|
||||
let need = |what: &str| {
|
||||
arg.ok_or_else(|| format!("GROUND choice {raw:?} needs a {what} argument"))
|
||||
};
|
||||
match raw {
|
||||
"restore_problem" => Ok(GroundChoice::RestoreProblem {
|
||||
problem: need("problem")? as u32,
|
||||
}),
|
||||
"cancel_attack" => Ok(GroundChoice::CancelAttack {
|
||||
attacker: PlayerId(need("seat")? as u8),
|
||||
}),
|
||||
"protect_problem" => Ok(GroundChoice::ProtectProblem {
|
||||
problem: need("problem")? as u32,
|
||||
}),
|
||||
"remove_blame" => Ok(GroundChoice::RemoveBlame {
|
||||
owner: PlayerId(need("seat")? as u8),
|
||||
}),
|
||||
"break_relation" => Ok(GroundChoice::BreakRelation {
|
||||
with: PlayerId(need("seat")? as u8),
|
||||
}),
|
||||
"reject_reverse" => Ok(GroundChoice::RejectReverse),
|
||||
other => Err(format!("unknown GROUND choice {other:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GR-L02/A05: how a Support target responds, chosen after Reveal.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SupportResponse {
|
||||
/// GR-L02: accept a Bond where no relation exists.
|
||||
AcceptBond,
|
||||
/// GR-L02: decline it; the Stress effect still applies.
|
||||
DeclineBond,
|
||||
/// GR-A05: turn an existing Rivalry into a Bond.
|
||||
FlipToBond,
|
||||
/// GR-A05: break the existing Rivalry.
|
||||
BreakRivalry,
|
||||
}
|
||||
|
||||
impl SupportResponse {
|
||||
fn parse(raw: &str) -> Result<Self, String> {
|
||||
match raw {
|
||||
"accept_bond" => Ok(SupportResponse::AcceptBond),
|
||||
"decline_bond" => Ok(SupportResponse::DeclineBond),
|
||||
"flip_to_bond" => Ok(SupportResponse::FlipToBond),
|
||||
"break_rivalry" => Ok(SupportResponse::BreakRivalry),
|
||||
other => Err(format!("unknown support response {other:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GroundMode {
|
||||
fn parse(raw: &str) -> Result<Self, String> {
|
||||
match raw {
|
||||
|
|
@ -233,7 +320,12 @@ pub enum GroundCommand {
|
|||
SpendFreedom,
|
||||
/// GR-R05: a player who revealed GROUND chooses its mode after
|
||||
/// seeing all revealed Actions.
|
||||
ChooseGroundMode { mode: GroundMode },
|
||||
ChooseGroundMode {
|
||||
mode: GroundMode,
|
||||
choice: Option<GroundChoice>,
|
||||
},
|
||||
/// GR-L02/A05: respond to a Support aimed at this player.
|
||||
RespondToSupport { response: SupportResponse },
|
||||
/// GR-R04: reveal all selections simultaneously. System-driven.
|
||||
Reveal,
|
||||
/// GR-R06/R07: resolve revealed Actions in fixed step order, Lead
|
||||
|
|
@ -280,10 +372,29 @@ pub enum GroundEvent {
|
|||
attacker: PlayerId,
|
||||
target: PlayerId,
|
||||
},
|
||||
/// GR-R05.
|
||||
/// GR-R05/A11/A12.
|
||||
GroundModeChosen {
|
||||
player: PlayerId,
|
||||
mode: GroundMode,
|
||||
choice: Option<GroundChoice>,
|
||||
},
|
||||
/// GR-L02/A05.
|
||||
SupportAnswered {
|
||||
player: PlayerId,
|
||||
response: SupportResponse,
|
||||
},
|
||||
/// GR-A11: a Denied Problem was restored face up.
|
||||
ProblemRestored {
|
||||
problem: u32,
|
||||
},
|
||||
/// GR-A11: a face-up Problem is protected from Deny this round.
|
||||
ProblemProtected {
|
||||
problem: u32,
|
||||
},
|
||||
/// GR-A12/T02: a Blame token was removed and returned to its owner.
|
||||
BlameRemoved {
|
||||
player: PlayerId,
|
||||
owner: PlayerId,
|
||||
},
|
||||
/// GR-A01: a hidden Problem was turned face up.
|
||||
ProblemRevealed {
|
||||
|
|
@ -482,7 +593,7 @@ impl Aggregate for GroundState {
|
|||
}
|
||||
// GR-R05: only after Reveal, only for a player who revealed
|
||||
// GROUND, once.
|
||||
GroundCommand::ChooseGroundMode { mode } => {
|
||||
GroundCommand::ChooseGroundMode { mode, choice } => {
|
||||
if self.step != RoundStep::Reveal {
|
||||
return Err(Rejection::NotAllowedNow);
|
||||
}
|
||||
|
|
@ -500,9 +611,72 @@ impl Aggregate for GroundState {
|
|||
return Err(Rejection::DuplicateCommand);
|
||||
}
|
||||
let _ = player;
|
||||
// GR-A10..A12: GR takes no sub-choice; OU and ND each
|
||||
// require one drawn from their own list.
|
||||
match (mode, choice) {
|
||||
(GroundMode::Gr, None) => {}
|
||||
(GroundMode::Gr, Some(_)) => {
|
||||
return Err(Rejection::Game {
|
||||
code: "unexpected-choice".into(),
|
||||
detail: "GR-A10: GROUND—GR takes no sub-choice".into(),
|
||||
})
|
||||
}
|
||||
(wanted, Some(choice)) if choice.mode() == *wanted => {}
|
||||
(wanted, _) => {
|
||||
return Err(Rejection::Game {
|
||||
code: "bad-choice".into(),
|
||||
detail: format!("GR-A11/A12: {wanted:?} needs one of its own choices"),
|
||||
})
|
||||
}
|
||||
}
|
||||
self.check_ground_choice(id, *choice)?;
|
||||
Ok(vec![GroundEvent::GroundModeChosen {
|
||||
player: id,
|
||||
mode: *mode,
|
||||
choice: *choice,
|
||||
}])
|
||||
}
|
||||
// GR-L02/A05: only the target of a revealed Support answers,
|
||||
// once, and only with a response its relation admits.
|
||||
GroundCommand::RespondToSupport { response } => {
|
||||
if self.step != RoundStep::Reveal {
|
||||
return Err(Rejection::NotAllowedNow);
|
||||
}
|
||||
let supporter = self.selections.iter().find(|(seat, sel)| {
|
||||
sel.action == Action::Support && sel.target == Some(id) && **seat != id
|
||||
});
|
||||
let Some((supporter, _)) = supporter else {
|
||||
return Err(Rejection::Game {
|
||||
code: "no-support-received".into(),
|
||||
detail: "GR-L02: no revealed Support targets this player".into(),
|
||||
});
|
||||
};
|
||||
if self.support_responses.contains_key(&id) {
|
||||
return Err(Rejection::DuplicateCommand);
|
||||
}
|
||||
let admitted = match self.relation_between(*supporter, id) {
|
||||
// GR-L02: no relation — the target may accept a Bond.
|
||||
None => matches!(
|
||||
response,
|
||||
SupportResponse::AcceptBond | SupportResponse::DeclineBond
|
||||
),
|
||||
// GR-A05: through a Rivalry — flip it or break it.
|
||||
Some(Relation::Rivalry) => matches!(
|
||||
response,
|
||||
SupportResponse::FlipToBond | SupportResponse::BreakRivalry
|
||||
),
|
||||
// GR-A04: through a Bond — nothing to answer.
|
||||
Some(Relation::Bond) => false,
|
||||
};
|
||||
if !admitted {
|
||||
return Err(Rejection::Game {
|
||||
code: "bad-response".into(),
|
||||
detail: format!("GR-L02/A05: {response:?} is not available here"),
|
||||
});
|
||||
}
|
||||
Ok(vec![GroundEvent::SupportAnswered {
|
||||
player: id,
|
||||
response: *response,
|
||||
}])
|
||||
}
|
||||
GroundCommand::Reveal | GroundCommand::Resolve | GroundCommand::EndRound => {
|
||||
|
|
@ -544,8 +718,36 @@ impl Aggregate for GroundState {
|
|||
state.protection = state.protection.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
GroundEvent::GroundModeChosen { player, mode } => {
|
||||
GroundEvent::GroundModeChosen {
|
||||
player,
|
||||
mode,
|
||||
choice,
|
||||
} => {
|
||||
self.ground_modes.insert(*player, *mode);
|
||||
if let Some(choice) = choice {
|
||||
self.ground_choices.insert(*player, *choice);
|
||||
}
|
||||
}
|
||||
GroundEvent::SupportAnswered { player, response } => {
|
||||
self.support_responses.insert(*player, *response);
|
||||
}
|
||||
GroundEvent::ProblemRestored { problem } => {
|
||||
if let Some(state) = self.problems.get_mut(problem) {
|
||||
state.denied = false;
|
||||
state.face_up = true;
|
||||
}
|
||||
}
|
||||
GroundEvent::ProblemProtected { problem } => {
|
||||
if let Some(state) = self.problems.get_mut(problem) {
|
||||
state.protected_this_round = true;
|
||||
}
|
||||
}
|
||||
GroundEvent::BlameRemoved { player, owner } => {
|
||||
if let Some(state) = self.players.get_mut(player) {
|
||||
if let Some(pos) = state.blame_from.iter().position(|o| o == owner) {
|
||||
state.blame_from.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
GroundEvent::ProblemRevealed { problem } => {
|
||||
if let Some(state) = self.problems.get_mut(problem) {
|
||||
|
|
@ -589,6 +791,8 @@ impl Aggregate for GroundState {
|
|||
self.lead = *next_lead;
|
||||
self.selections.clear();
|
||||
self.ground_modes.clear();
|
||||
self.ground_choices.clear();
|
||||
self.support_responses.clear();
|
||||
for state in self.players.values_mut() {
|
||||
// GR-R03: the gate lift lasts one Select step only.
|
||||
state.freedom_gate_lifted = false;
|
||||
|
|
@ -619,6 +823,10 @@ impl GroundState {
|
|||
// (GR-L05): a Bond formed now is not "pre-existing".
|
||||
let pre_existing = self.relations.clone();
|
||||
|
||||
// GR-A11: Attacks cancelled by a GROUND—OU choice this round.
|
||||
let mut ou_cancels: std::collections::BTreeSet<(PlayerId, PlayerId)> =
|
||||
std::collections::BTreeSet::new();
|
||||
|
||||
// Step 1 — GROUND (GR-A10..A12).
|
||||
for actor in self.seat_order() {
|
||||
if self.selections.get(&actor).map(|s| s.action) != Some(Action::Ground) {
|
||||
|
|
@ -638,8 +846,41 @@ impl GroundState {
|
|||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
// GR-A11/A12 need a sub-choice command; pending.
|
||||
Some(GroundMode::Ou) | Some(GroundMode::Nd) | None => {}
|
||||
// GR-A11: Observe & Uphold.
|
||||
Some(GroundMode::Ou) => match work.ground_choices.get(&actor).copied() {
|
||||
Some(GroundChoice::RestoreProblem { problem }) => {
|
||||
events.push(GroundEvent::ProblemRestored { problem });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
Some(GroundChoice::ProtectProblem { problem }) => {
|
||||
events.push(GroundEvent::ProblemProtected { problem });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
// Consumed in the Attack step below.
|
||||
Some(GroundChoice::CancelAttack { attacker }) => {
|
||||
ou_cancels.insert((attacker, actor));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
// GR-A12: Name & Decide.
|
||||
Some(GroundMode::Nd) => match work.ground_choices.get(&actor).copied() {
|
||||
Some(GroundChoice::RemoveBlame { owner }) => {
|
||||
events.push(GroundEvent::BlameRemoved {
|
||||
player: actor,
|
||||
owner,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
Some(GroundChoice::BreakRelation { with }) => {
|
||||
events.push(GroundEvent::RelationBroken {
|
||||
pair: Pair::new(actor, with),
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
// GR-D05: consumed by the Reverse stage.
|
||||
_ => {}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -669,9 +910,8 @@ impl GroundState {
|
|||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
// GR-A05: Support through a Rivalry. The target's
|
||||
// flip-or-break choice needs a consent command and is
|
||||
// deferred; only the Stress effect applies here.
|
||||
// GR-A05: Support through a Rivalry — −1 Stress, then
|
||||
// the target flips it to a Bond or breaks it.
|
||||
Some(Relation::Rivalry) => {
|
||||
let stress = work.stress_after(target, -1);
|
||||
events.push(GroundEvent::StressSet {
|
||||
|
|
@ -679,9 +919,25 @@ impl GroundState {
|
|||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
let pair = Pair::new(actor, target);
|
||||
match work.support_responses.get(&target).copied() {
|
||||
Some(SupportResponse::FlipToBond) => {
|
||||
events.push(GroundEvent::RelationFormed {
|
||||
pair,
|
||||
relation: Relation::Bond,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
Some(SupportResponse::BreakRivalry) => {
|
||||
events.push(GroundEvent::RelationBroken { pair });
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
// No answer: the Rivalry stands.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// GR-A03: no relation. GR-L02 requires the target's
|
||||
// consent to form a Bond, so no relation forms here.
|
||||
// GR-A03/L02: no relation — −1 Stress, and a Bond forms
|
||||
// only if the target accepts and both have a free slot.
|
||||
None => {
|
||||
let stress = work.stress_after(target, -1);
|
||||
events.push(GroundEvent::StressSet {
|
||||
|
|
@ -689,6 +945,15 @@ impl GroundState {
|
|||
stress,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
let accepted = work.support_responses.get(&target).copied()
|
||||
== Some(SupportResponse::AcceptBond);
|
||||
if accepted && work.has_free_slot(actor) && work.has_free_slot(target) {
|
||||
events.push(GroundEvent::RelationFormed {
|
||||
pair: Pair::new(actor, target),
|
||||
relation: Relation::Bond,
|
||||
});
|
||||
work.fold(events.last().expect("just pushed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -705,7 +970,13 @@ impl GroundState {
|
|||
continue;
|
||||
};
|
||||
|
||||
// GR-A09: Protection absorbs the Attack entirely.
|
||||
// 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,
|
||||
|
|
@ -880,6 +1151,73 @@ impl GroundState {
|
|||
events
|
||||
}
|
||||
|
||||
/// GR-A11/A12: the sub-choice must name something that exists and
|
||||
/// is in a state the choice can act on.
|
||||
fn check_ground_choice(
|
||||
&self,
|
||||
actor: PlayerId,
|
||||
choice: Option<GroundChoice>,
|
||||
) -> Result<(), Rejection> {
|
||||
let bad = |detail: String| Rejection::Game {
|
||||
code: "bad-choice".into(),
|
||||
detail,
|
||||
};
|
||||
let problem = |id: u32| {
|
||||
self.problems
|
||||
.get(&id)
|
||||
.ok_or_else(|| bad(format!("no Problem {id}")))
|
||||
};
|
||||
match choice {
|
||||
None => Ok(()),
|
||||
// GR-A11: only a Denied Problem can be restored.
|
||||
Some(GroundChoice::RestoreProblem { problem: id }) => {
|
||||
if !problem(id)?.denied {
|
||||
return Err(bad(format!("GR-A11: Problem {id} is not Denied")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// GR-A11: only a face-up Problem can be protected.
|
||||
Some(GroundChoice::ProtectProblem { problem: id }) => {
|
||||
if !problem(id)?.face_up || problem(id)?.denied {
|
||||
return Err(bad(format!("GR-A11: Problem {id} is not face up")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// GR-A11: the cancelled Attack must actually target us.
|
||||
Some(GroundChoice::CancelAttack { attacker }) => {
|
||||
let aimed_here = self
|
||||
.selections
|
||||
.get(&attacker)
|
||||
.is_some_and(|s| s.action == Action::Attack && s.target == Some(actor));
|
||||
if !aimed_here {
|
||||
return Err(bad(format!(
|
||||
"GR-A11: {attacker} is not attacking this player"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// GR-A12/T02: the Blame token must be in front of us.
|
||||
Some(GroundChoice::RemoveBlame { owner }) => {
|
||||
let held = self
|
||||
.players
|
||||
.get(&actor)
|
||||
.is_some_and(|p| p.blame_from.contains(&owner));
|
||||
if !held {
|
||||
return Err(bad(format!("GR-T02: no Blame token from {owner} here")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// GR-A12: the relation must exist and involve us.
|
||||
Some(GroundChoice::BreakRelation { with }) => {
|
||||
if self.relation_between(actor, with).is_none() {
|
||||
return Err(bad(format!("GR-A12: no relation with {with}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(GroundChoice::RejectReverse) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// GR-A13 targeting legality, shared by every Action.
|
||||
fn check_targeting(
|
||||
&self,
|
||||
|
|
@ -1027,6 +1365,8 @@ impl ScenarioGame for GroundState {
|
|||
step: RoundStep::Select,
|
||||
selections: BTreeMap::new(),
|
||||
ground_modes: BTreeMap::new(),
|
||||
ground_choices: BTreeMap::new(),
|
||||
support_responses: BTreeMap::new(),
|
||||
seed,
|
||||
})
|
||||
}
|
||||
|
|
@ -1060,6 +1400,16 @@ impl ScenarioGame for GroundState {
|
|||
"spend_freedom" => GroundCommand::SpendFreedom,
|
||||
"choose_ground_mode" => GroundCommand::ChooseGroundMode {
|
||||
mode: GroundMode::parse(&arg_str("mode")?)?,
|
||||
choice: match step.args.get("choice") {
|
||||
Some(_) => Some(GroundChoice::parse(
|
||||
&arg_str("choice")?,
|
||||
arg_u64("problem").or_else(|| arg_u64("seat")),
|
||||
)?),
|
||||
None => None,
|
||||
},
|
||||
},
|
||||
"respond_to_support" => GroundCommand::RespondToSupport {
|
||||
response: SupportResponse::parse(&arg_str("response")?)?,
|
||||
},
|
||||
"reveal" => GroundCommand::Reveal,
|
||||
"resolve" => GroundCommand::Resolve,
|
||||
|
|
@ -1103,6 +1453,8 @@ mod tests {
|
|||
step: RoundStep::Select,
|
||||
selections: BTreeMap::new(),
|
||||
ground_modes: BTreeMap::new(),
|
||||
ground_choices: BTreeMap::new(),
|
||||
support_responses: BTreeMap::new(),
|
||||
seed: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
scenarios/ground/gr-a05-support-response.yaml
Normal file
57
scenarios/ground/gr-a05-support-response.yaml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
scenario: ground/gr-a05-support-response
|
||||
description: >
|
||||
A Support target answers after Reveal: through a Rivalry they flip it
|
||||
to a Bond or break it (GR-A05); with no relation a Bond forms only if
|
||||
they accept (GR-L02). A player not receiving Support cannot answer.
|
||||
covers: [GR-A05, GR-L02, GR-A03]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"relations.0-1": Rivalry
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: SUPPORT, target: P2 }
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args: { action: SUPPORT, target: P3 }
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args: { action: INVESTIGATE, problem: 3 }
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
# GR-A05: P2 is supported through a Rivalry, so it may flip.
|
||||
- actor: P2
|
||||
cmd: respond_to_support
|
||||
args: { response: flip_to_bond }
|
||||
# GR-L02: P3 has no relation with its supporter, so it accepts.
|
||||
- actor: P3
|
||||
cmd: respond_to_support
|
||||
args: { response: accept_bond }
|
||||
# P1 received no Support this round.
|
||||
- actor: P1
|
||||
cmd: respond_to_support
|
||||
args: { response: accept_bond }
|
||||
# GR-A05: a flip is not available where no relation exists.
|
||||
- actor: P3
|
||||
cmd: respond_to_support
|
||||
args: { response: flip_to_bond }
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: SupportAnswered
|
||||
player: 1
|
||||
response: FlipToBond
|
||||
- kind: RelationFormed
|
||||
relation: Bond
|
||||
state:
|
||||
"relations.0-1": Bond
|
||||
"relations.1-2": Bond
|
||||
"players.1.stress": 1
|
||||
"players.2.stress": 1
|
||||
rejects: [6, 7]
|
||||
43
scenarios/ground/gr-a11-ground-ou.yaml
Normal file
43
scenarios/ground/gr-a11-ground-ou.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
scenario: ground/gr-a11-ground-ou
|
||||
description: >
|
||||
GROUND—OU restores a Denied Problem (GR-A11), and a Denied Problem is
|
||||
not otherwise revealable (GR-P02). A mode's sub-choice must belong to
|
||||
that mode (GR-A10).
|
||||
covers: [GR-A11, GR-P02, GR-P01]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"problems.2.denied": true
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: GROUND }
|
||||
- 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-A10: GROUND—GR takes no sub-choice.
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args: { mode: GR, choice: restore_problem, problem: 2 }
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args: { mode: OU, choice: restore_problem, problem: 2 }
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: ProblemRestored
|
||||
problem: 2
|
||||
state:
|
||||
"problems.2.denied": false
|
||||
"problems.2.face_up": true
|
||||
rejects: [4]
|
||||
47
scenarios/ground/gr-a12-ground-nd.yaml
Normal file
47
scenarios/ground/gr-a12-ground-nd.yaml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
scenario: ground/gr-a12-ground-nd
|
||||
description: >
|
||||
GROUND—ND removes a Blame token from its player (GR-A12, GR-T02). The
|
||||
choice must name a token that is actually there, and a relation break
|
||||
must name a relation that exists.
|
||||
covers: [GR-A12, GR-T02, GR-T03]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"lead": 0
|
||||
"players.0.blame_from": [1]
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: GROUND }
|
||||
- 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-T02: P3 has no Blame token in front of P1.
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args: { mode: ND, choice: remove_blame, seat: 2 }
|
||||
# GR-A12: no relation with P2 exists to break.
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args: { mode: ND, choice: break_relation, seat: 1 }
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args: { mode: ND, choice: remove_blame, seat: 1 }
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
expect:
|
||||
events:
|
||||
- kind: BlameRemoved
|
||||
player: 0
|
||||
owner: 1
|
||||
state:
|
||||
"players.0.blame_from": []
|
||||
rejects: [4, 5]
|
||||
Loading…
Add table
Add a link
Reference in a new issue