Compare commits
2 commits
650f9b3a0b
...
8ae68195b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ae68195b8 | |||
| d713fbc1bc |
5 changed files with 382 additions and 6 deletions
|
|
@ -253,9 +253,9 @@
|
||||||
| task | CB-WP-0047-T01 | done | — | workplans/CB-WP-0047-all-four-boards-and-all-three-modes.md |
|
| task | CB-WP-0047-T01 | done | — | workplans/CB-WP-0047-all-four-boards-and-all-three-modes.md |
|
||||||
| task | CB-WP-0048-T00 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
| task | CB-WP-0048-T00 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
||||||
| task | CB-WP-0048-T01 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
| task | CB-WP-0048-T01 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
||||||
| task | CB-WP-0048-T02 | todo | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
| task | CB-WP-0048-T02 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
||||||
| task | CB-WP-0048-T03 | todo | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
| task | CB-WP-0048-T03 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
||||||
| task | CB-WP-0048-T04 | todo | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
| task | CB-WP-0048-T04 | done | — | workplans/CB-WP-0048-a-configuration-not-a-variant.md |
|
||||||
| task | CB-WP-0049-T01 | done | — | workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md |
|
| task | CB-WP-0049-T01 | done | — | workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md |
|
||||||
| task | CB-WP-0049-T02 | done | — | workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md |
|
| task | CB-WP-0049-T02 | done | — | workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md |
|
||||||
| task | CB-WP-0049-T03 | done | — | workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md |
|
| task | CB-WP-0049-T03 | done | — | workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md |
|
||||||
|
|
|
||||||
|
|
@ -1249,6 +1249,113 @@ impl Policy for ObjectivePolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A seat that attends to the modules in play (CB-WP-0049 T04).
|
||||||
|
///
|
||||||
|
/// ## One policy, not one per module
|
||||||
|
///
|
||||||
|
/// The obvious shape is `H2AwarePolicy` — and that is the blob schema 2
|
||||||
|
/// exists to retire, rebuilt one layer up. This reads the **resolved
|
||||||
|
/// `Rules`**, so a configuration carrying two modules gets both terms,
|
||||||
|
/// and a module the kernel gains later is one arm here rather than a new
|
||||||
|
/// policy for every combination it appears in.
|
||||||
|
///
|
||||||
|
/// ## It delegates twice
|
||||||
|
///
|
||||||
|
/// `ObjectivePolicy::rank` (which delegates to `GreedyPolicy::rank`) is
|
||||||
|
/// the base, so this differs from an objective-aware seat in **exactly**
|
||||||
|
/// the arms named below and nowhere else. That is what makes
|
||||||
|
/// `a_module_aware_seat_is_ordinary_when_no_module_is_live` a real
|
||||||
|
/// control rather than a hope.
|
||||||
|
///
|
||||||
|
/// ## Both terms read the table
|
||||||
|
///
|
||||||
|
/// `stress_scope` and the owner marker are **public regardless of the
|
||||||
|
/// card's face** — the H2 rules say to place the owner marker on the
|
||||||
|
/// card, and at a table everyone can see it. So this is blind
|
||||||
|
/// (ADR-0023) without needing to reason about hidden Problems.
|
||||||
|
pub struct ModuleAwarePolicy;
|
||||||
|
|
||||||
|
impl ModuleAwarePolicy {
|
||||||
|
/// Does this Problem's End-of-Round Stress fall on `seat`?
|
||||||
|
fn presses(state: &GroundState, seat: PlayerId, problem: u32) -> bool {
|
||||||
|
use crate::edition::StressScope;
|
||||||
|
let Some(p) = state.problems.get(&problem) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match p.scope {
|
||||||
|
None | Some(StressScope::Global) => true,
|
||||||
|
Some(StressScope::Personal) => p.owner == Some(seat),
|
||||||
|
Some(StressScope::Bond) => p
|
||||||
|
.owner
|
||||||
|
.is_some_and(|o| state.bond_network(o).contains(&seat)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 {
|
||||||
|
let base = ObjectivePolicy::rank(state, seat, cmd);
|
||||||
|
let rules = state.rules();
|
||||||
|
match cmd {
|
||||||
|
// **Clear the Problem that is pressing ME.** Under
|
||||||
|
// `problem_stress.scoped` an unclaimed Problem ticks only the
|
||||||
|
// seats in its scope, so two equally valuable cards are not
|
||||||
|
// equally urgent — which is the whole point of the module and
|
||||||
|
// the thing no previous bot could see.
|
||||||
|
GroundCommand::SelectAction {
|
||||||
|
action: Action::Solve,
|
||||||
|
problem: Some(n),
|
||||||
|
..
|
||||||
|
} if rules.problem_stress == crate::config::ProblemStress::Scoped => {
|
||||||
|
if Self::presses(state, seat, *n) {
|
||||||
|
base + 8
|
||||||
|
} else {
|
||||||
|
base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// **ATTACK is a Stress tool at the gate.** Under
|
||||||
|
// `attack_relief.self_soothe_ge4` an uncancelled ATTACK by a
|
||||||
|
// seat at Stress >= 4 sheds 1. Greedy ranks ATTACK at 10 in
|
||||||
|
// every position because for the printed rules it does
|
||||||
|
// nothing for the attacker.
|
||||||
|
GroundCommand::SelectAction {
|
||||||
|
action: Action::Attack,
|
||||||
|
..
|
||||||
|
} if rules.attack_relief == crate::config::AttackRelief::SelfSootheGe4 => {
|
||||||
|
let gated = state
|
||||||
|
.players
|
||||||
|
.get(&seat)
|
||||||
|
.is_some_and(|p| p.stress >= 4 && !p.freedom_gate_lifted);
|
||||||
|
if gated {
|
||||||
|
base + 55
|
||||||
|
} else {
|
||||||
|
base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Policy for ModuleAwarePolicy {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"module-aware"
|
||||||
|
}
|
||||||
|
fn choose(
|
||||||
|
&mut self,
|
||||||
|
state: &GroundState,
|
||||||
|
seat: PlayerId,
|
||||||
|
legal: &[GroundCommand],
|
||||||
|
_may_pass: bool,
|
||||||
|
) -> Choice {
|
||||||
|
let mut best = 0;
|
||||||
|
for (i, c) in legal.iter().enumerate() {
|
||||||
|
if Self::rank(state, seat, c) > Self::rank(state, seat, &legal[best]) {
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Choice::Command(best)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// **A policy is bound by what its seat can see** ([ADR-0023]).
|
/// **A policy is bound by what its seat can see** ([ADR-0023]).
|
||||||
///
|
///
|
||||||
/// `Policy::choose` takes the whole `GroundState`, which carries every
|
/// `Policy::choose` takes the whole `GroundState`, which carries every
|
||||||
|
|
@ -1436,6 +1543,8 @@ mod blindness_tests {
|
||||||
for seat in st.players.keys().copied() {
|
for seat in st.players.keys().copied() {
|
||||||
is_blind(|| ObjectivePolicy, &st, seat)
|
is_blind(|| ObjectivePolicy, &st, seat)
|
||||||
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
||||||
|
is_blind(|| ModuleAwarePolicy, &st, seat)
|
||||||
|
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1550,6 +1659,231 @@ mod blindness_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Two face-up, unclaimed, equally valuable Problems that `seat` can
|
||||||
|
/// solve either of — the position where a preference between SOLVEs
|
||||||
|
/// can actually change a choice.
|
||||||
|
///
|
||||||
|
/// **Built, because it does not occur at a fresh deal**: only the
|
||||||
|
/// Surface Problem is face up in round one, so there is one SOLVE
|
||||||
|
/// candidate and no ranking term can move the argmax. A control that
|
||||||
|
/// sweeps fresh deals cannot see a module term fire at all.
|
||||||
|
fn two_solvable_problems(config: &str) -> (GroundState, PlayerId, u32, u32) {
|
||||||
|
use crate::edition::StressScope;
|
||||||
|
let mut st = deal(4, 5).with_config(crate::config::Configuration::select(config).unwrap());
|
||||||
|
let seat = PlayerId(0);
|
||||||
|
let keys: Vec<u32> = st.problems.keys().copied().take(2).collect();
|
||||||
|
for (i, k) in keys.iter().enumerate() {
|
||||||
|
let p = st.problems.get_mut(k).unwrap();
|
||||||
|
p.face_up = true;
|
||||||
|
p.denied = false;
|
||||||
|
p.claimed_by = None;
|
||||||
|
p.value = 2;
|
||||||
|
p.suit = crate::Suit::Repair;
|
||||||
|
p.scope = Some(StressScope::Personal);
|
||||||
|
p.owner = Some(if i == 0 { PlayerId(1) } else { seat });
|
||||||
|
}
|
||||||
|
st.players.get_mut(&seat).unwrap().hand = vec![
|
||||||
|
crate::SolutionCard {
|
||||||
|
suit: crate::Suit::Repair,
|
||||||
|
},
|
||||||
|
crate::SolutionCard {
|
||||||
|
suit: crate::Suit::Repair,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
(st, seat, keys[1], keys[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **A module-aware seat is ordinary when no module is live**
|
||||||
|
/// (CB-WP-0049 T04).
|
||||||
|
///
|
||||||
|
/// The control that separates "attends to the module" from "is
|
||||||
|
/// simply a different bot". Compared against `ObjectivePolicy`, not
|
||||||
|
/// greedy, so it isolates module-awareness from objective-awareness:
|
||||||
|
/// the two differ in exactly the arms `ModuleAwarePolicy` names, and
|
||||||
|
/// under the baseline none of them fire.
|
||||||
|
#[test]
|
||||||
|
fn a_module_aware_seat_is_ordinary_when_no_module_is_live() {
|
||||||
|
for seed in [1u64, 7, 42, 99] {
|
||||||
|
for players in [2u8, 4, 6] {
|
||||||
|
for mode in [
|
||||||
|
ScoringMode::SharedGround,
|
||||||
|
ScoringMode::CommonProblem,
|
||||||
|
ScoringMode::BondedCoalitions,
|
||||||
|
] {
|
||||||
|
let mut st = deal(players, seed);
|
||||||
|
st.mode = mode;
|
||||||
|
assert!(
|
||||||
|
st.config.active_modules().is_empty(),
|
||||||
|
"the fixture must be the baseline for this control to mean anything"
|
||||||
|
);
|
||||||
|
for seat in st.players.keys().copied() {
|
||||||
|
let legal = legal_commands(&st, seat);
|
||||||
|
if legal.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
ObjectivePolicy.choose(&st, seat, &legal, false),
|
||||||
|
ModuleAwarePolicy.choose(&st, seat, &legal, false),
|
||||||
|
"{players}p {mode:?} seed {seed} seat {seat:?}: the two \
|
||||||
|
diverged with no module live"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **The sweep above cannot fail, and mutation said so.** Forcing
|
||||||
|
// the scope term to fire regardless of configuration left it
|
||||||
|
// green: at a fresh deal one SOLVE is legal, so no ranking term
|
||||||
|
// can move the argmax. A control that cannot distinguish the
|
||||||
|
// property from its negation is not a control (ADR-0006 D3).
|
||||||
|
//
|
||||||
|
// This is the same position the divergence test uses, played
|
||||||
|
// under the BASELINE. There the two policies must agree, and a
|
||||||
|
// module term that fires anyway shows up immediately.
|
||||||
|
let (st, seat, mine, theirs) = two_solvable_problems("ground-darvo-r0");
|
||||||
|
assert!(
|
||||||
|
st.config.active_modules().is_empty(),
|
||||||
|
"the control must run with no module live"
|
||||||
|
);
|
||||||
|
let solve = |n: u32| GroundCommand::SelectAction {
|
||||||
|
action: Action::Solve,
|
||||||
|
target: None,
|
||||||
|
problem: Some(n),
|
||||||
|
};
|
||||||
|
for n in [mine, theirs] {
|
||||||
|
assert_eq!(
|
||||||
|
ObjectivePolicy::rank(&st, seat, &solve(n)),
|
||||||
|
ModuleAwarePolicy::rank(&st, seat, &solve(n)),
|
||||||
|
"with no module live the module-aware seat ranked SOLVE on \
|
||||||
|
Problem {n} differently"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **And it is NOT ordinary when one is** — or the control above
|
||||||
|
/// passes by the policy doing nothing at all.
|
||||||
|
///
|
||||||
|
/// **The position is built, not hoped for.** The first version swept
|
||||||
|
/// fresh deals and found zero divergence, correctly: at a fresh deal
|
||||||
|
/// only the Surface Problem is face up, so there is never a *choice*
|
||||||
|
/// between two SOLVEs for scope to decide, and no ranking term can
|
||||||
|
/// matter where there is one candidate. That is a real limit on when
|
||||||
|
/// the module reaches a decision, and it is recorded rather than
|
||||||
|
/// tuned away — the term is **inert at round-one positions**.
|
||||||
|
#[test]
|
||||||
|
fn a_module_aware_seat_prefers_the_problem_that_presses_it() {
|
||||||
|
let (st, seat, mine, theirs) = two_solvable_problems("h2");
|
||||||
|
let solve = |n: u32| GroundCommand::SelectAction {
|
||||||
|
action: Action::Solve,
|
||||||
|
target: None,
|
||||||
|
problem: Some(n),
|
||||||
|
};
|
||||||
|
// The objective-aware seat is indifferent: same value, same suit.
|
||||||
|
assert_eq!(
|
||||||
|
ObjectivePolicy::rank(&st, seat, &solve(mine)),
|
||||||
|
ObjectivePolicy::rank(&st, seat, &solve(theirs)),
|
||||||
|
"the two Problems must be identical to a seat that cannot see scope, \
|
||||||
|
or this test is measuring something else"
|
||||||
|
);
|
||||||
|
// The module-aware seat is not.
|
||||||
|
assert!(
|
||||||
|
ModuleAwarePolicy::rank(&st, seat, &solve(mine))
|
||||||
|
> ModuleAwarePolicy::rank(&st, seat, &solve(theirs)),
|
||||||
|
"the seat did not prefer the Problem whose Stress falls on it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **ATTACK becomes a Stress tool under `attack_relief`.**
|
||||||
|
#[test]
|
||||||
|
fn a_module_aware_seat_attacks_to_shed_stress_only_under_that_module() {
|
||||||
|
let attack = GroundCommand::SelectAction {
|
||||||
|
action: Action::Attack,
|
||||||
|
target: Some(PlayerId(1)),
|
||||||
|
problem: None,
|
||||||
|
};
|
||||||
|
let at_gate = |cfg: &str| {
|
||||||
|
let mut st = deal(4, 5).with_config(crate::config::Configuration::select(cfg).unwrap());
|
||||||
|
st.players.get_mut(&PlayerId(0)).unwrap().stress = 5;
|
||||||
|
st
|
||||||
|
};
|
||||||
|
let baseline = at_gate("ground-darvo-r0");
|
||||||
|
let soothe = at_gate("h1");
|
||||||
|
assert_eq!(
|
||||||
|
ModuleAwarePolicy::rank(&baseline, PlayerId(0), &attack),
|
||||||
|
ObjectivePolicy::rank(&baseline, PlayerId(0), &attack),
|
||||||
|
"ATTACK gained a bonus with no attack_relief module live"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ModuleAwarePolicy::rank(&soothe, PlayerId(0), &attack)
|
||||||
|
> ObjectivePolicy::rank(&soothe, PlayerId(0), &attack),
|
||||||
|
"under attack_relief.self_soothe_ge4 a gated seat did not value \
|
||||||
|
ATTACK as the Stress tool the module makes it"
|
||||||
|
);
|
||||||
|
// Below the gate it sheds nothing, so it is worth no more.
|
||||||
|
let mut calm = at_gate("h1");
|
||||||
|
calm.players.get_mut(&PlayerId(0)).unwrap().stress = 1;
|
||||||
|
assert_eq!(
|
||||||
|
ModuleAwarePolicy::rank(&calm, PlayerId(0), &attack),
|
||||||
|
ObjectivePolicy::rank(&calm, PlayerId(0), &attack),
|
||||||
|
"a calm seat valued ATTACK as relief it would not receive"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The scope term must name the seats the RULE names.
|
||||||
|
#[test]
|
||||||
|
fn the_scope_term_follows_the_editions_scope_rule() {
|
||||||
|
use crate::edition::StressScope;
|
||||||
|
let mut st = deal(4, 5).with_config(crate::config::Configuration::select("h2").unwrap());
|
||||||
|
let seats: Vec<PlayerId> = st.players.keys().copied().collect();
|
||||||
|
let key = *st.problems.keys().next().unwrap();
|
||||||
|
|
||||||
|
// personal: the owner and nobody else.
|
||||||
|
{
|
||||||
|
let p = st.problems.get_mut(&key).unwrap();
|
||||||
|
p.scope = Some(StressScope::Personal);
|
||||||
|
p.owner = Some(seats[1]);
|
||||||
|
}
|
||||||
|
assert!(ModuleAwarePolicy::presses(&st, seats[1], key));
|
||||||
|
assert!(!ModuleAwarePolicy::presses(&st, seats[0], key));
|
||||||
|
|
||||||
|
// global: everyone, owner or not.
|
||||||
|
st.problems.get_mut(&key).unwrap().scope = Some(StressScope::Global);
|
||||||
|
for s in &seats {
|
||||||
|
assert!(
|
||||||
|
ModuleAwarePolicy::presses(&st, *s, key),
|
||||||
|
"global missed {s:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bond: the owner's network. With no Bonds that is the owner
|
||||||
|
// alone — the rule's own fallback, not a rendering convenience.
|
||||||
|
{
|
||||||
|
let p = st.problems.get_mut(&key).unwrap();
|
||||||
|
p.scope = Some(StressScope::Bond);
|
||||||
|
p.owner = Some(seats[1]);
|
||||||
|
}
|
||||||
|
st.relations.clear();
|
||||||
|
assert!(ModuleAwarePolicy::presses(&st, seats[1], key));
|
||||||
|
assert!(!ModuleAwarePolicy::presses(&st, seats[2], key));
|
||||||
|
st.relations
|
||||||
|
.insert(crate::Pair::new(seats[1], seats[2]), crate::Relation::Bond);
|
||||||
|
assert!(
|
||||||
|
ModuleAwarePolicy::presses(&st, seats[2], key),
|
||||||
|
"a Bond partner is in the network and shares the tick"
|
||||||
|
);
|
||||||
|
// Rivalry does NOT expand the network (Rules_Text.csv row 20).
|
||||||
|
st.relations.clear();
|
||||||
|
st.relations.insert(
|
||||||
|
crate::Pair::new(seats[1], seats[3]),
|
||||||
|
crate::Relation::Rivalry,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!ModuleAwarePolicy::presses(&st, seats[3], key),
|
||||||
|
"a Rivalry expanded bond scope; the edition says it does not"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// And the shipped policies are blind.
|
/// And the shipped policies are blind.
|
||||||
#[test]
|
#[test]
|
||||||
fn every_shipped_policy_is_blind_to_what_its_seat_cannot_see() {
|
fn every_shipped_policy_is_blind_to_what_its_seat_cannot_see() {
|
||||||
|
|
|
||||||
|
|
@ -403,7 +403,7 @@ impl GroundState {
|
||||||
/// where that is refused loudly; by the time a rule is being applied
|
/// where that is refused loudly; by the time a rule is being applied
|
||||||
/// the game is already in progress, and a mid-game panic would turn
|
/// the game is already in progress, and a mid-game panic would turn
|
||||||
/// a selection error into a crash at an unrelated moment.
|
/// a selection error into a crash at an unrelated moment.
|
||||||
fn rules(&self) -> crate::config::Rules {
|
pub fn rules(&self) -> crate::config::Rules {
|
||||||
self.config.resolve().unwrap_or_default()
|
self.config.resolve().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
id: CB-WP-0048
|
id: CB-WP-0048
|
||||||
kind: product
|
kind: product
|
||||||
title: "A configuration, not a variant"
|
title: "A configuration, not a variant"
|
||||||
status: active
|
status: done
|
||||||
state_hub_workstream_id: "44ceb8d8-e6e2-46d6-a694-fd275804b771"
|
state_hub_workstream_id: "44ceb8d8-e6e2-46d6-a694-fd275804b771"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ mode-aware.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: CB-WP-0049-T04
|
id: CB-WP-0049-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: normal
|
priority: normal
|
||||||
state_hub_task_id: "1b26c7e9-e049-49d0-aecd-63830c7c8a37"
|
state_hub_task_id: "1b26c7e9-e049-49d0-aecd-63830c7c8a37"
|
||||||
```
|
```
|
||||||
|
|
@ -236,6 +236,48 @@ achievable cheaply, the honest move is one policy that reads the resolved
|
||||||
control that separates "attends to the module" from "plays differently";
|
control that separates "attends to the module" from "plays differently";
|
||||||
- **blind** (T01).
|
- **blind** (T01).
|
||||||
|
|
||||||
|
**Done 2026-08-08.** `ModuleAwarePolicy` reads the resolved `Rules` —
|
||||||
|
**one policy, not one per module.** `H2AwarePolicy` would have been the
|
||||||
|
blob schema 2 exists to retire, rebuilt a layer up; reading `Rules` means
|
||||||
|
a configuration carrying two modules gets both terms and a module the
|
||||||
|
kernel gains later is one arm here rather than a new policy for every
|
||||||
|
combination it appears in.
|
||||||
|
|
||||||
|
It delegates to `ObjectivePolicy::rank`, which delegates to
|
||||||
|
`GreedyPolicy::rank`, so it differs in exactly two arms:
|
||||||
|
|
||||||
|
| module | term |
|
||||||
|
|---|---|
|
||||||
|
| `problem_stress.scoped` | prefer the Problem whose Stress falls **on me** — two equally valuable cards are not equally urgent, which is the module's whole point |
|
||||||
|
| `attack_relief.self_soothe_ge4` | at the gate, ATTACK is a Stress tool; greedy ranks it 10 in every position because under the printed rules it does nothing for the attacker |
|
||||||
|
|
||||||
|
Both read the table: `stress_scope` and the owner marker are public
|
||||||
|
regardless of the card's face, because the H2 rules say to place the
|
||||||
|
marker **on the card** and at a table everyone can see it. So it is blind
|
||||||
|
(ADR-0023) by construction, and the T01 sweep now covers it.
|
||||||
|
|
||||||
|
### The control I claimed was real was vacuous, and mutation said so
|
||||||
|
|
||||||
|
*"Under the baseline the two must be identical"* swept fresh deals across
|
||||||
|
four seeds × three seat bands × three modes — and **forcing the scope
|
||||||
|
term to fire regardless of configuration left it green.**
|
||||||
|
|
||||||
|
At a fresh deal only the Surface Problem is face up, so exactly one SOLVE
|
||||||
|
is legal and **no ranking term can move the argmax**. The control could
|
||||||
|
not distinguish the property from its negation, which is the definition
|
||||||
|
of decoration (ADR-0006 D3).
|
||||||
|
|
||||||
|
Both tests now use a **built** position — two face-up, unclaimed, equally
|
||||||
|
valuable Problems the seat can solve either of, differing only in scope —
|
||||||
|
played under H2 for the divergence and under the baseline for the
|
||||||
|
control. The mutation goes red there.
|
||||||
|
|
||||||
|
**This is also a finding about the module, not only about the test.** The
|
||||||
|
scope term is **inert at round-one positions**: the module cannot reach a
|
||||||
|
decision until more than one Problem is solvable, so anything measuring
|
||||||
|
H2 with round-one-heavy play is measuring a mechanism that has not
|
||||||
|
started. Recorded rather than tuned away.
|
||||||
|
|
||||||
## Two things this pass fixed that were not the task
|
## Two things this pass fixed that were not the task
|
||||||
|
|
||||||
**A test keyed on a word another repo owns.** T01's controls found their
|
**A test keyed on a word another repo owns.** T01's controls found their
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue