CB-WP-0037 T02: the engine's rules against the edition's
Some checks failed
ci / check (push) Failing after 3s
Some checks failed
ci / check (push) Failing after 3s
Vendored DARVO.csv, Relations.csv and Scenarios.csv — the three unread files that carry mechanism. The engine has implemented the DARVO sequence and relation formation/breaking from GroundRules.md since the beginning, and had never read the game owner's statement of them. Every checkable clause agrees, and agreement is recorded rather than noted: a survey that finds nothing and leaves no trace cannot be told from one never run. The two hardest clauses to notice were already right — Focus placed "even if the Attack was cancelled", and Focus removed when the sequence ends before REVERSE. The tests are tripwires, not derivations. The match was made by a person reading prose, and that reading goes stale in silence when the prose changes; each behaviour pins the phrase it was read from, so a reworded edition goes red and asks for a human. Mutation-proven by rewording the cancelled-Attack clause. F25 raised, and it is the real yield: Scenarios.csv carries threshold_2_players/3_4/5_6, starting_stress and round_track, and the engine hardcodes all three — a match returning 5/7/9, stress: 2 at setup, five rounds. They agree on all four scenarios. These are the most contested numbers in the project; the whole 4/6/9 vs 5/7/9 episode turned on them, and the engine has been right by maintenance coincidence rather than by reading the file that owns them. Also pinned: Problems.csv and Scenarios.csv both state the deal and the engine reads only the first. They agree; nothing was checking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d98580fb4c
commit
53109ec5e5
7 changed files with 423 additions and 1 deletions
4
editions/ground-darvo-r0/DARVO.csv
Normal file
4
editions/ground-darvo-r0/DARVO.csv
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
stage_order,stage,symbol_id,mandatory_effect,target_memory,advance
|
||||
1,DENY,SYM_DENY,"Choose one face-up, unsolved Problem not protected by GROUND—OU. Turn it face down and place a Denied token on it. Ordinary Investigate cannot reveal it.",None.,Advance to ATTACK unless the sequence is ended by GROUND or Bond Support.
|
||||
2,ATTACK,SYM_DARVO_ATTACK,"Make one extra Attack against any other player. Resolve it with the normal relation rules. Then place your Focus/Blame token Focus-side beside that target, even if the Attack was cancelled.",The Focus target becomes the target of REVERSE.,Advance to REVERSE unless the sequence is ended; remove Focus if the sequence ends before REVERSE.
|
||||
3,REVERSE,SYM_REVERSE,"Target the player holding your Focus. Unless they reject it with GROUND—ND, flip your Focus token to Blame in front of them, give them +1 Stress, and take one Protection token. Then reduce your own Stress by 2.",Focus becomes Blame and remains with the target until removed by GROUND—ND.,End the DARVO sequence.
|
||||
|
|
|
@ -19,6 +19,15 @@ build must not depend on a sibling checkout that CI does not have.
|
|||
| `Solutions.csv` | 2026-08-06 | `4fa3b8ed` | Solution titles and microcopy |
|
||||
| `Modes.csv` | 2026-08-06 | `4fa3b8ed` | mode text and `scoring_tiebreak` |
|
||||
| `Tokens.csv` | 2026-08-07 | `4fa3b8ed` | the components: `front_text`, `quantity`, `sides` (ADR-0016) |
|
||||
| `DARVO.csv` | 2026-08-08 | `8886543` | **the DARVO sequence as rules**: `mandatory_effect`, `target_memory`, `advance` (CB-WP-0037 T02) |
|
||||
| `Relations.csv` | 2026-08-08 | `8886543` | **how Bonds and Rivalries form and break**: `formation`, `breaking`, `rules_text` |
|
||||
| `Scenarios.csv` | 2026-08-08 | `8886543` | the four scenarios and their setups — the engine deals one |
|
||||
|
||||
**Why these three, now.** The engine already implements the DARVO
|
||||
sequence and relation formation/breaking — from `GroundRules.md`, never
|
||||
from the edition's own statement of them. That is not inert display data;
|
||||
it is **rules taken from a secondary source and never checked against the
|
||||
primary one**, and CB-WP-0037 T02 exists to check them.
|
||||
|
||||
**Deliberately absent**: `BOM`, `Print_Manifest`, `Back_Designs`,
|
||||
`Symbols`, `Design_Tokens` — production artifacts for a physical print
|
||||
|
|
@ -30,8 +39,11 @@ that this engine plays the edition as printed (ADR-0015 D2).
|
|||
|
||||
```
|
||||
sha256 7a7a302aabecdeb419c562a029bec3e3576a306b0bd1175d1200de60d9530073 Actions.csv
|
||||
sha256 0afb882dc8001f03bc48a152c9d8c36ad492ceeff2d30ef6a3a690937317f77b DARVO.csv
|
||||
sha256 565431571bc06adafb67edefd4b368839b6b94134c2289877384688cc643390b Modes.csv
|
||||
sha256 0a04830c93b62dcb2f4411a9fbde576368a7427fe9e63c5015e606e4d42d23a0 Problems.csv
|
||||
sha256 cd84c3b131c1b7a541e9e42529fdce2b7911f0832cce8c50cfcb8462d01dc551 Relations.csv
|
||||
sha256 f830de9ca44417a7510e04a39ea24a070a81969b33a31fa68361fba132ad40cd Scenarios.csv
|
||||
sha256 0bda1ee97de726b5e8c4404ab15ecc53a359e6c23db74fb41ecb51d78f9884ad Solutions.csv
|
||||
sha256 25273bb6e74c9ad545c5e89dfe0f76b294d0a6cbef51f80d548039d5f0943484 Tokens.csv
|
||||
```
|
||||
|
|
|
|||
3
editions/ground-darvo-r0/Relations.csv
Normal file
3
editions/ground-darvo-r0/Relations.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
relation_side,symbol_id,title,rules_text,formation,breaking
|
||||
Bond,SYM_BOND,BOND,"Support through an existing Bond: −2 Stress, ready Freedom, cancel the target's current DARVO stage, and end that sequence. Attack through a Bond: +2 Stress and flip this tile to Rivalry.",A no-relation Support may create a Bond if both players have a free relation slot and the target accepts.,GROUND—ND may break it. An Attack flips it rather than breaking it.
|
||||
Rivalry,SYM_RIVALRY,RIVALRY,Support through Rivalry: −1 Stress; the target chooses to flip the tile to Bond or break it. Attack through Rivalry: +2 Stress and break the relation.,A no-relation Attack creates a Rivalry automatically if both players have a free relation slot.,"A further Attack, a repairing Support rejected as Bond, or GROUND—ND may break it."
|
||||
|
5
editions/ground-darvo-r0/Scenarios.csv
Normal file
5
editions/ground-darvo-r0/Scenarios.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
scenario_id,title,premise,surface_problem_id,hidden_problem_ids,setup_2_players,setup_3_4_players,setup_5_6_players,threshold_2_players,threshold_3_4_players,threshold_5_6_players,starting_stress,round_track
|
||||
SCN_01,Missed Deadline,"The team promised a delivery. It did not arrive when expected, and the conversation about why is becoming personal.",PRB_01_S,PRB_01_1 | PRB_01_2 | PRB_01_3 | PRB_01_4,Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 1–5 printed along the bottom edge.
|
||||
SCN_02,Shared Responsibility,A shared responsibility keeps being left undone. Everyone agrees it matters; nobody agrees on what fairness requires.,PRB_02_S,PRB_02_1 | PRB_02_2 | PRB_02_3 | PRB_02_4,Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 1–5 printed along the bottom edge.
|
||||
SCN_03,Decision Without Consent,A decision affecting the group was announced as settled. Several players believe they were consulted only after the fact.,PRB_03_S,PRB_03_1 | PRB_03_2 | PRB_03_3 | PRB_03_4,Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 1–5 printed along the bottom edge.
|
||||
SCN_04,Broken Confidence,"Information shared in confidence travelled beyond the expected circle. Harm, intent, and responsibility are now contested.",PRB_04_S,PRB_04_1 | PRB_04_2 | PRB_04_3 | PRB_04_4,Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 1–5 printed along the bottom edge.
|
||||
|
|
|
@ -72,6 +72,9 @@ const ACTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Action
|
|||
const SOLUTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Solutions.csv");
|
||||
const MODES_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Modes.csv");
|
||||
const TOKENS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Tokens.csv");
|
||||
const DARVO_CSV: &str = include_str!("../../../editions/ground-darvo-r0/DARVO.csv");
|
||||
const RELATIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Relations.csv");
|
||||
const SCENARIOS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Scenarios.csv");
|
||||
|
||||
/// A vendored CSV, parsed into rows addressable by column name.
|
||||
///
|
||||
|
|
@ -410,6 +413,119 @@ pub fn deal(scenario_id: &str, players: u8) -> Result<Vec<EditionProblem>, Strin
|
|||
Ok(dealt)
|
||||
}
|
||||
|
||||
/// One DARVO stage as the edition states it (CB-WP-0037 T02).
|
||||
///
|
||||
/// **The engine already implements this sequence**, from `GroundRules.md`
|
||||
/// and not from here. Vendoring it does not change behaviour; it makes
|
||||
/// the behaviour checkable against the source that owns it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DarvoStageText {
|
||||
pub order: u8,
|
||||
pub stage: String,
|
||||
pub mandatory_effect: String,
|
||||
pub target_memory: String,
|
||||
pub advance: String,
|
||||
}
|
||||
|
||||
/// The DARVO sequence, in stage order.
|
||||
pub fn darvo_stages() -> Result<Vec<DarvoStageText>, String> {
|
||||
let t = Table::parse(DARVO_CSV, "DARVO.csv")?;
|
||||
let mut out = Vec::new();
|
||||
for row in &t.rows {
|
||||
out.push(DarvoStageText {
|
||||
order: t
|
||||
.get(row, "stage_order")?
|
||||
.parse()
|
||||
.map_err(|_| "stage_order is not a number".to_string())?,
|
||||
stage: t.get(row, "stage")?.to_string(),
|
||||
mandatory_effect: t.get(row, "mandatory_effect")?.to_string(),
|
||||
target_memory: t.get(row, "target_memory")?.to_string(),
|
||||
advance: t.get(row, "advance")?.to_string(),
|
||||
});
|
||||
}
|
||||
out.sort_by_key(|s| s.order);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A scenario as the edition states it (CB-WP-0037 T02).
|
||||
///
|
||||
/// **This file states the deal a second time.** `Problems.csv` carries
|
||||
/// `visibility` and `hidden_priority`; this carries the same board as
|
||||
/// explicit id lists. The engine deals from the former, so the two are a
|
||||
/// pair that can drift.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScenarioText {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub surface_problem_id: String,
|
||||
pub hidden_problem_ids: Vec<String>,
|
||||
/// GR-E01 by seat band, as the EDITION states it. The engine has its
|
||||
/// own copy in `GroundState::threshold` (F25).
|
||||
pub thresholds: (u32, u32, u32),
|
||||
/// Prose, e.g. "All players start at Stress 2."
|
||||
pub starting_stress: String,
|
||||
/// Prose, e.g. "Rounds 1-5 printed along the bottom edge."
|
||||
pub round_track: String,
|
||||
}
|
||||
|
||||
/// The four scenarios. **The engine deals `SCN_01` and only `SCN_01`**
|
||||
/// (ADR-0015 D5) — this is what says so out loud.
|
||||
pub fn scenarios() -> Result<Vec<ScenarioText>, String> {
|
||||
let t = Table::parse(SCENARIOS_CSV, "Scenarios.csv")?;
|
||||
let mut out = Vec::new();
|
||||
for row in &t.rows {
|
||||
out.push(ScenarioText {
|
||||
id: t.get(row, "scenario_id")?.to_string(),
|
||||
title: t.get(row, "title")?.to_string(),
|
||||
surface_problem_id: t.get(row, "surface_problem_id")?.to_string(),
|
||||
hidden_problem_ids: t
|
||||
.get(row, "hidden_problem_ids")?
|
||||
.split('|')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
thresholds: (
|
||||
t.get(row, "threshold_2_players")?
|
||||
.parse()
|
||||
.map_err(|_| "threshold_2_players is not a number".to_string())?,
|
||||
t.get(row, "threshold_3_4_players")?
|
||||
.parse()
|
||||
.map_err(|_| "threshold_3_4_players is not a number".to_string())?,
|
||||
t.get(row, "threshold_5_6_players")?
|
||||
.parse()
|
||||
.map_err(|_| "threshold_5_6_players is not a number".to_string())?,
|
||||
),
|
||||
starting_stress: t.get(row, "starting_stress")?.to_string(),
|
||||
round_track: t.get(row, "round_track")?.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// One relation side as the edition states it (CB-WP-0037 T02).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RelationText {
|
||||
pub side: String,
|
||||
pub rules_text: String,
|
||||
pub formation: String,
|
||||
pub breaking: String,
|
||||
}
|
||||
|
||||
/// Bond and Rivalry, with how each forms and breaks.
|
||||
pub fn relations() -> Result<Vec<RelationText>, String> {
|
||||
let t = Table::parse(RELATIONS_CSV, "Relations.csv")?;
|
||||
let mut out = Vec::new();
|
||||
for row in &t.rows {
|
||||
out.push(RelationText {
|
||||
side: t.get(row, "relation_side")?.to_string(),
|
||||
rules_text: t.get(row, "rules_text")?.to_string(),
|
||||
formation: t.get(row, "formation")?.to_string(),
|
||||
breaking: t.get(row, "breaking")?.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The core Solution deck, 6 per suit in canonical order (GR-S04).
|
||||
pub fn solution_deck() -> Vec<SolutionCard> {
|
||||
[Suit::Clarify, Suit::Repair, Suit::Boundary, Suit::Change]
|
||||
|
|
@ -548,6 +664,9 @@ mod card_text_tests {
|
|||
let _ = solutions();
|
||||
let _ = modes();
|
||||
let _ = tokens();
|
||||
let _ = darvo_stages();
|
||||
let _ = relations();
|
||||
let _ = scenarios();
|
||||
|
||||
let files = [
|
||||
("Problems.csv", CSV),
|
||||
|
|
@ -555,6 +674,9 @@ mod card_text_tests {
|
|||
("Solutions.csv", SOLUTIONS_CSV),
|
||||
("Modes.csv", MODES_CSV),
|
||||
("Tokens.csv", TOKENS_CSV),
|
||||
("DARVO.csv", DARVO_CSV),
|
||||
("Relations.csv", RELATIONS_CSV),
|
||||
("Scenarios.csv", SCENARIOS_CSV),
|
||||
];
|
||||
let mut report = String::new();
|
||||
let mut total_unread = 0usize;
|
||||
|
|
@ -586,6 +708,222 @@ mod card_text_tests {
|
|||
println!("{report}");
|
||||
}
|
||||
|
||||
/// **F25** — the engine hardcodes numbers the edition states.
|
||||
///
|
||||
/// `GroundState::threshold` is a `match` returning 5/7/9;
|
||||
/// `Scenarios.csv` carries `threshold_2_players`,
|
||||
/// `threshold_3_4_players` and `threshold_5_6_players`. Setup writes
|
||||
/// `stress: 2`; the edition says *"All players start at Stress 2."*
|
||||
/// The engine runs five rounds; the edition prints a 1-5 track.
|
||||
///
|
||||
/// **They agree, on every scenario** — and that is the point, not the
|
||||
/// reassurance. These are the most contested numbers in the project:
|
||||
/// the whole 4/6/9 versus 5/7/9 episode turned on them, and the
|
||||
/// engine has been right about them by maintenance coincidence rather
|
||||
/// than by reading the file that owns them.
|
||||
///
|
||||
/// Same shape as F24's solution deck. `inert`, role `default`: green
|
||||
/// because they agree, red the moment either side moves.
|
||||
#[test]
|
||||
fn the_engine_agrees_with_the_editions_own_numbers() {
|
||||
for s in scenarios().expect("Scenarios.csv") {
|
||||
// Mirrors `GroundState::threshold`'s bands exactly.
|
||||
assert_eq!(
|
||||
s.thresholds,
|
||||
(5, 7, 9),
|
||||
"{}: the edition's thresholds moved away from the engine's hardcoded 5/7/9",
|
||||
s.id
|
||||
);
|
||||
assert!(
|
||||
s.starting_stress.contains("Stress 2"),
|
||||
"{}: setup writes stress: 2 because of this line, which now reads {:?}",
|
||||
s.id,
|
||||
s.starting_stress
|
||||
);
|
||||
assert!(
|
||||
s.round_track.contains("1\u{2013}5") || s.round_track.contains("1-5"),
|
||||
"{}: the engine plays five rounds because of this line, which now reads {:?}",
|
||||
s.id,
|
||||
s.round_track
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Two files state the same deal, and the engine reads one.**
|
||||
///
|
||||
/// `Problems.csv` carries `visibility` and `hidden_priority`;
|
||||
/// `Scenarios.csv` carries `surface_problem_id` and
|
||||
/// `hidden_problem_ids` for the same four scenarios. They agree
|
||||
/// today, for every scenario — **and nothing was checking**, so an
|
||||
/// edit to one and not the other would have had the engine dealing a
|
||||
/// board the edition contradicts, silently.
|
||||
///
|
||||
/// Ours to check, not to rule on: if they ever disagree this is a
|
||||
/// question for `ground-game`, and the test says which file said what.
|
||||
#[test]
|
||||
fn the_two_files_that_state_the_deal_agree() {
|
||||
// Read Problems.csv here rather than through `EditionProblem`,
|
||||
// which carries no id: adding one would touch a struct that
|
||||
// feeds setup, and this test has no business changing state.
|
||||
let pt = Table::parse(CSV, "Problems.csv").expect("Problems.csv");
|
||||
for s in scenarios().expect("Scenarios.csv") {
|
||||
let rows: Vec<&Vec<String>> = pt
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|r| pt.get(r, "scenario_id").expect("scenario_id") == s.id)
|
||||
.collect();
|
||||
assert!(!rows.is_empty(), "{}: no Problems at all", s.id);
|
||||
|
||||
let surface: Vec<&str> = rows
|
||||
.iter()
|
||||
.filter(|r| pt.get(r, "visibility").expect("visibility") == "Surface")
|
||||
.map(|r| pt.get(r, "problem_id").expect("problem_id"))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
surface,
|
||||
vec![s.surface_problem_id.as_str()],
|
||||
"{}: Problems.csv and Scenarios.csv disagree on the Surface Problem",
|
||||
s.id
|
||||
);
|
||||
|
||||
let mut hidden: Vec<&str> = rows
|
||||
.iter()
|
||||
.filter(|r| pt.get(r, "visibility").expect("visibility") != "Surface")
|
||||
.map(|r| pt.get(r, "problem_id").expect("problem_id"))
|
||||
.collect();
|
||||
hidden.sort_unstable();
|
||||
let mut stated: Vec<&str> = s.hidden_problem_ids.iter().map(|x| x.as_str()).collect();
|
||||
stated.sort_unstable();
|
||||
assert_eq!(
|
||||
hidden, stated,
|
||||
"{}: Problems.csv and Scenarios.csv disagree on the hidden Problems",
|
||||
s.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **CB-WP-0037 T02 — the engine's rules against the edition's.**
|
||||
///
|
||||
/// The engine implements DARVO and relation behaviour from
|
||||
/// `GroundRules.md`. `DARVO.csv` and `Relations.csv` are where the
|
||||
/// game's owner states the same rules, and until 2026-08-08 nothing
|
||||
/// here had ever read them.
|
||||
///
|
||||
/// **Every checkable clause agrees**, including the two easiest to
|
||||
/// drop: Focus is placed *"even if the Attack was cancelled"*, and
|
||||
/// Focus is removed when the sequence ends before REVERSE.
|
||||
///
|
||||
/// **Agreement is recorded, not just noted.** A survey that finds
|
||||
/// nothing and leaves no trace cannot be told from one never run
|
||||
/// (CB-EV-0027 §3).
|
||||
///
|
||||
/// **These are tripwires, not derivations.** The behaviour was
|
||||
/// matched by a person reading prose, and that reading can go stale
|
||||
/// in silence when the prose changes. Pinning the phrase each
|
||||
/// behaviour was read from turns *"I checked it once"* into *"it is
|
||||
/// still what I checked"* — and when ground-game rewords a rule this
|
||||
/// goes red and asks for a human, which is the correct outcome, not
|
||||
/// an automatic re-derivation.
|
||||
#[test]
|
||||
fn the_edition_still_says_what_the_engine_implements() {
|
||||
let stages = darvo_stages().expect("DARVO.csv");
|
||||
assert_eq!(
|
||||
stages.iter().map(|s| s.stage.as_str()).collect::<Vec<_>>(),
|
||||
["DENY", "ATTACK", "REVERSE"],
|
||||
"the DARVO sequence changed shape"
|
||||
);
|
||||
let stage = |name: &str| {
|
||||
stages
|
||||
.iter()
|
||||
.find(|s| s.stage == name)
|
||||
.unwrap_or_else(|| panic!("no {name} stage"))
|
||||
.clone()
|
||||
};
|
||||
|
||||
// GR-D03 / lib.rs DarvoStage::Deny.
|
||||
let deny = stage("DENY");
|
||||
assert!(deny
|
||||
.mandatory_effect
|
||||
.contains("Turn it face down and place a Denied token"));
|
||||
assert!(deny
|
||||
.advance
|
||||
.contains("Advance to ATTACK unless the sequence is ended"));
|
||||
|
||||
// GR-D04 / lib.rs DarvoStage::Attack. Both clauses the engine
|
||||
// implements and a careless reading would lose.
|
||||
let attack = stage("ATTACK");
|
||||
assert!(
|
||||
attack
|
||||
.mandatory_effect
|
||||
.contains("even if the Attack was cancelled"),
|
||||
"the engine places Focus after a cancelled Attack because the edition says to"
|
||||
);
|
||||
assert!(attack
|
||||
.target_memory
|
||||
.contains("Focus target becomes the target of REVERSE"));
|
||||
assert!(
|
||||
attack
|
||||
.advance
|
||||
.contains("remove Focus if the sequence ends before REVERSE"),
|
||||
"`focus.remove` on DarvoEnded is this clause; if it is gone, so is the reason"
|
||||
);
|
||||
|
||||
// GR-D05 / lib.rs DarvoStage::Reverse: +1 to the target, a
|
||||
// Protection to the owner, then −2 to the owner.
|
||||
let reverse = stage("REVERSE");
|
||||
assert!(reverse
|
||||
.mandatory_effect
|
||||
.contains("+1 Stress, and take one Protection token"));
|
||||
assert!(reverse
|
||||
.mandatory_effect
|
||||
.contains("reduce your own Stress by 2"));
|
||||
assert!(reverse.advance.contains("End the DARVO sequence"));
|
||||
|
||||
let rels = relations().expect("Relations.csv");
|
||||
let side = |name: &str| {
|
||||
rels.iter()
|
||||
.find(|r| r.side == name)
|
||||
.unwrap_or_else(|| panic!("no {name} side"))
|
||||
.clone()
|
||||
};
|
||||
|
||||
// GR-A04 / GR-A07 against lib.rs's Support and attack paths.
|
||||
let bond = side("Bond");
|
||||
assert!(bond.rules_text.contains("\u{2212}2 Stress, ready Freedom"));
|
||||
assert!(
|
||||
bond.rules_text
|
||||
.contains("cancel the target's current DARVO stage"),
|
||||
"the bond_support branch that pushes DarvoEnded is this clause"
|
||||
);
|
||||
assert!(bond
|
||||
.rules_text
|
||||
.contains("Attack through a Bond: +2 Stress and flip this tile"));
|
||||
assert!(bond
|
||||
.formation
|
||||
.contains("free relation slot and the target accepts"));
|
||||
|
||||
// GR-A05 / GR-A08.
|
||||
let rivalry = side("Rivalry");
|
||||
assert!(rivalry
|
||||
.rules_text
|
||||
.contains("Support through Rivalry: \u{2212}1 Stress"));
|
||||
assert!(
|
||||
rivalry
|
||||
.rules_text
|
||||
.contains("the target chooses to flip the tile to Bond or break it"),
|
||||
"SupportResponse::FlipToBond / BreakRivalry are this clause"
|
||||
);
|
||||
assert!(rivalry
|
||||
.rules_text
|
||||
.contains("Attack through Rivalry: +2 Stress and break"));
|
||||
assert!(
|
||||
rivalry
|
||||
.formation
|
||||
.contains("creates a Rivalry automatically"),
|
||||
"the engine forms it without consent because the edition says automatically"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The import check ground-game's final ruling asked for**, at
|
||||
/// every seat band (`RULED GROUND-WP-0004`, 2026-08-03, engine ask 2):
|
||||
///
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
|||
| F16 | inconsistent | withdrawn | games/ground/examples/difficulty.rs | counterexample | 2026-08-05 | clay-borg |
|
||||
| F17 | degenerate | raised | games/ground/examples/attack-value.rs | counterexample | 2026-08-06 | ground-game |
|
||||
| F18 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_engine_reads_only_part_of_what_it_vendored` | counterexample | 2026-08-06 | clay-borg |
|
||||
| F25 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_engine_agrees_with_the_editions_own_numbers` | default | 2026-08-08 | clay-borg |
|
||||
| F24 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_hardcoded_deck_still_matches_the_edition` | default | 2026-08-07 | clay-borg |
|
||||
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
|
||||
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
|
||||
|
|
@ -202,6 +203,19 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
|||
sees `Clarify` where the card reads *"Ask What Happened — Invite a
|
||||
concrete account before judging."* **`inert`**: the data exists and
|
||||
cannot fire, because nothing reads it. **Ours, and CB-WP-0028 fixes it.**
|
||||
- **F25 — the engine hardcodes numbers the edition states.**
|
||||
`GroundState::threshold` is a `match` returning 5/7/9 while
|
||||
`Scenarios.csv` carries `threshold_2_players`, `threshold_3_4_players`
|
||||
and `threshold_5_6_players`; setup writes `stress: 2` against *"All
|
||||
players start at Stress 2."*; the engine plays five rounds against a
|
||||
printed 1–5 track. **All three agree, on all four scenarios**, and that
|
||||
is the finding rather than the reassurance: these are the most contested
|
||||
numbers in the project — the whole 4/6/9 versus 5/7/9 episode turned on
|
||||
them — and the engine has been right by maintenance coincidence rather
|
||||
than by reading the file that owns them. **`inert`**, role `default`:
|
||||
green because they agree, red the moment either side moves. **Ours.**
|
||||
Same shape as F24.
|
||||
|
||||
- **F24 — the draw pile is a Rust literal.** `solution_deck()` builds six
|
||||
of each suit from an array and never opens `Solutions.csv`, whose `suit`
|
||||
and `quantity` columns say the same thing. **They agree today** — 24
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ deletes the literal, and it turns red the moment either side moves.
|
|||
|
||||
```task
|
||||
id: CB-WP-0037-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "625b51fd-8af1-46bf-8b91-087760259dd6"
|
||||
```
|
||||
|
|
@ -124,6 +124,52 @@ mechanism.
|
|||
trace cannot be told apart from one never run (CB-EV-0027 §3);
|
||||
- AM-4's dependency and size budgets hold, or the pass says what it cost.
|
||||
|
||||
**Done 2026-08-08.** `DARVO.csv`, `Relations.csv` and `Scenarios.csv`
|
||||
vendored with digests; `make edition-check` green both ways.
|
||||
|
||||
**Every checkable clause agrees.** Recorded rather than merely noted,
|
||||
because a survey that finds nothing and leaves no trace cannot be told
|
||||
from one never run:
|
||||
|
||||
| the edition says | the engine does |
|
||||
|---|---|
|
||||
| Bond Support: −2 Stress, ready Freedom, **cancel the target's DARVO stage** | −2, `FreedomReadied`, `DarvoEnded` |
|
||||
| Bond Attack: +2 and **flip** to Rivalry | +2, `RelationFormed(Rivalry)` |
|
||||
| Rivalry Support: −1, **target chooses** flip or break | −1, `FlipToBond` / `BreakRivalry` |
|
||||
| Rivalry Attack: +2 and **break** | +2, `RelationBroken` |
|
||||
| no-relation Attack forms a Rivalry **automatically** | forms without consent, both slots free |
|
||||
| DARVO ATTACK places Focus **even if the Attack was cancelled** | `FocusPlaced` after `resolve_attack` regardless |
|
||||
| **remove Focus if the sequence ends before REVERSE** | `focus.remove` on `DarvoEnded` |
|
||||
| REVERSE: flip to Blame, +1 to target, take a Protection, −2 to self | exactly that, with GROUND—ND rejection |
|
||||
|
||||
**The two hardest clauses to notice were already right**, which is the
|
||||
part worth saying: nobody had read the file, and they were right anyway.
|
||||
|
||||
**These are tripwires, not derivations.** The match was made by a person
|
||||
reading prose, and that reading goes stale in silence when the prose
|
||||
changes. Each behaviour pins the phrase it was read from, so a reworded
|
||||
edition turns the test red and asks for a human — mutation-proven by
|
||||
rewording *"even if the Attack was cancelled"*, which fails naming the
|
||||
exact engine branch that depends on it.
|
||||
|
||||
**F25 raised, and it is the real yield.** `Scenarios.csv` carries
|
||||
`threshold_2_players/3_4/5_6`, `starting_stress` and `round_track` — and
|
||||
the engine **hardcodes all three**: a `match` returning 5/7/9, `stress: 2`
|
||||
at setup, five rounds. They agree on all four scenarios. **These are the
|
||||
most contested numbers in the project** — the entire 4/6/9 versus 5/7/9
|
||||
episode turned on them — and the engine has been right about them by
|
||||
maintenance coincidence rather than by reading the file that owns them.
|
||||
Same shape as F24; `inert`, role `default`.
|
||||
|
||||
**A second pair that could drift silently**: `Problems.csv` and
|
||||
`Scenarios.csv` both state the deal, and the engine reads only the first.
|
||||
`the_two_files_that_state_the_deal_agree` checks all four scenarios; they
|
||||
match today, and nothing was checking.
|
||||
|
||||
**Coverage, re-measured** — the earlier figure would otherwise have
|
||||
understated the gap by three files: `DARVO.csv` 5/6, `Relations.csv` 4/6,
|
||||
`Scenarios.csv` 4/13.
|
||||
|
||||
## Task: classify the rest
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue