CB-WP-0049 T02/T03: a seat that plays its objective, and F27 splits in two
Some checks failed
ci / check (push) Has been cancelled
Some checks failed
ci / check (push) Has been cancelled
objective() reads GroundState::score (now public) rather than restating
what winning is; a copy in the bot would disagree with the kernel the
first time ground-game rules on F28.
Working out WHERE the modes can differ was most of the task and it
bounds the result: SOLVE always claims for the actor, so own-score and
group-score want the same SOLVE nearly everywhere. That is a fact about
GROUND's action set, not a shortcoming of the bot. Two real divergences,
both readable off the table: SUPPORT regulates someone else (worth less
against a rival, worth MORE under coalitions where a Bond merges them
into my side), and SOLVE's value is the card's value, which greedy
ignores entirely.
THE RESULT — F27 splits in two:
group success UNCHANGED in 34 of 36 cells
who wins MOVES: BONDED COALITIONS at 4p goes 2.04 -> 2.98,
2.12 -> 3.29, 2.05 -> 3.01 winning seats per game
So "the competitive modes are scoring lenses over cooperative play" was
too strong and is withdrawn. The sharper claim: GROUND's scoring modes
change WHO WINS, not WHETHER THE GROUP SUCCEEDS. And the effect is
seat-band dependent -- 2p none, 4p largest, 6p none under coalitions;
two relation slots capping network growth is a candidate explanation and
is untested.
The panel now prints BOTH policies side by side. That was a correction
mid-task: the first version printed only the new one and I compared it
against a figure remembered from CB-WP-0047 -- a comparison against a
board nobody re-ran.
Control that makes the numbers mean anything: under SHARED GROUND the
two policies agree at all but <=2 decision points across 12 boards, so a
moving column is mode-awareness and not simply a different bot.
Also: two T01 tests keyed on `status: proposed`, which ground-game
renamed to `ready-for-implement` mid-session. They now find the module
by asking resolve() -- the structural property is ours and does not move
when another repo edits its vocabulary.
Also: `make vendor` replaces three hand re-vendors with a tool that
regenerates digests by walking editions/, and reports one-sided files
rather than resolving them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
82b9e7df31
commit
3045eb03f8
16 changed files with 859 additions and 94 deletions
|
|
@ -17,7 +17,7 @@
|
|||
//! and the column is expected to match, which is itself a control.
|
||||
|
||||
use cb_game_runtime::{ScenarioGame, Setup};
|
||||
use games_ground::bot::{play, GreedyPolicy, Policy};
|
||||
use games_ground::bot::{play, GreedyPolicy, ObjectivePolicy, Policy};
|
||||
use games_ground::{GroundState, ScoringMode};
|
||||
|
||||
/// Games per cell. Named once so the banner and the assertion cannot
|
||||
|
|
@ -41,7 +41,17 @@ struct Cell {
|
|||
setup_fails: u32,
|
||||
}
|
||||
|
||||
fn sweep(scenario: &str, mode: ScoringMode, players: u8) -> Cell {
|
||||
/// Which bot fills every seat. CB-WP-0049 T03: F27 says the three modes
|
||||
/// produce identical play because greedy never reads `state.mode`; the
|
||||
/// only way to know whether that is a fact about GROUND or a fact about
|
||||
/// our bot is to run the same panel with a seat that does read it.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Seat {
|
||||
Greedy,
|
||||
Objective,
|
||||
}
|
||||
|
||||
fn sweep(scenario: &str, mode: ScoringMode, players: u8, who: Seat) -> Cell {
|
||||
let mut c = Cell {
|
||||
games: 0,
|
||||
played: 0,
|
||||
|
|
@ -74,7 +84,10 @@ fn sweep(scenario: &str, mode: ScoringMode, players: u8) -> Cell {
|
|||
// scenario it did not play.
|
||||
assert_eq!(st.scenario, scenario, "{preset} dealt {}", st.scenario);
|
||||
let mut ps: Vec<Box<dyn Policy>> = (0..players)
|
||||
.map(|_| Box::new(GreedyPolicy) as Box<dyn Policy>)
|
||||
.map(|_| match who {
|
||||
Seat::Greedy => Box::new(GreedyPolicy) as Box<dyn Policy>,
|
||||
Seat::Objective => Box::new(ObjectivePolicy) as Box<dyn Policy>,
|
||||
})
|
||||
.collect();
|
||||
let g = match play(st, &mut ps) {
|
||||
Ok(g) => g,
|
||||
|
|
@ -131,16 +144,23 @@ fn main() {
|
|||
for players in [2u8, 4, 6] {
|
||||
println!("{players} players");
|
||||
println!(
|
||||
" {:<32} {:<18} {:>5} {:>7} {:>6} {:>9}",
|
||||
"scenario", "mode", "won", "win/g", "pts", "threshold"
|
||||
" {:<32} {:<18} {:>5} {:>5} {:>7} {:>7} {:>6} {:>9}",
|
||||
"scenario", "mode", "grdy", "obj", "win/g g", "win/g o", "pts", "threshold"
|
||||
);
|
||||
for s in &scenarios {
|
||||
for (label, mode) in &modes {
|
||||
let c = sweep(&s.id, *mode, players);
|
||||
let g = sweep(&s.id, *mode, players, Seat::Greedy);
|
||||
let c = sweep(&s.id, *mode, players, Seat::Objective);
|
||||
println!(
|
||||
" {:<32} {label} {:>5} {:>7.2} {:>6.1} {:>9}",
|
||||
" {:<32} {label} {:>5} {:>5} {:>7.2} {:>7.2} {:>6.1} {:>9}",
|
||||
format!("{} {}", s.id, s.title),
|
||||
g.group_success,
|
||||
c.group_success,
|
||||
// **Both, side by side.** Reading one policy's number
|
||||
// against a figure remembered from an earlier pass is
|
||||
// how a comparison gets made against a board nobody
|
||||
// re-ran.
|
||||
f64::from(g.winners) / f64::from(g.games),
|
||||
f64::from(c.winners) / f64::from(c.games),
|
||||
f64::from(c.total) / f64::from(c.games),
|
||||
c.threshold,
|
||||
|
|
@ -150,6 +170,11 @@ fn main() {
|
|||
println!();
|
||||
}
|
||||
|
||||
println!("`grdy` is GreedyPolicy, which never reads state.mode; `obj`");
|
||||
println!("is ObjectivePolicy, which plays its seat's own objective.");
|
||||
println!("If the obj column varies ACROSS modes where grdy does not,");
|
||||
println!("F27 was a fact about our bot. If it does not, it is a fact");
|
||||
println!("about GROUND.\n");
|
||||
println!("SCN_01 and SCN_02 are the same board — identical suits and");
|
||||
println!("values at every priority. Their rows are expected to MATCH,");
|
||||
println!("and a divergence means one of the two decks was edited.");
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
use crate::{
|
||||
Action, DarvoTarget, GroundChoice, GroundCommand, GroundMode, GroundState, Relation, RoundStep,
|
||||
Selection, SupportResponse,
|
||||
ScoringMode, Selection, SupportResponse,
|
||||
};
|
||||
use cb_kernel::{Actor, Aggregate, ChaChaRng, KernelRng, PlayerId, Rejection, Seed};
|
||||
|
||||
|
|
@ -1100,6 +1100,155 @@ mod tests {
|
|||
// than removed, and carries why the numbers changed.
|
||||
}
|
||||
|
||||
/// What a seat is trying to maximise (CB-WP-0049 T02).
|
||||
///
|
||||
/// **Read off `GroundState::score`, never restated.** The game already
|
||||
/// computes all three answers to build its `Outcome`; a copy in the bot
|
||||
/// would be a second definition of winning, and the two would disagree
|
||||
/// the first time ground-game rules on F28.
|
||||
///
|
||||
/// | mode | objective |
|
||||
/// |---|---|
|
||||
/// | SHARED GROUND | the group total |
|
||||
/// | COMMON PROBLEM | own claimed value − own Blame |
|
||||
/// | BONDED COALITIONS | own coalition's summed personal score |
|
||||
///
|
||||
/// It reads only `claimed_by`, `value` of **claimed** Problems and Blame
|
||||
/// tokens — all of which are on the table — so it is blind by
|
||||
/// construction (ADR-0023) rather than by inspection.
|
||||
pub fn objective(state: &GroundState, seat: PlayerId) -> i32 {
|
||||
let o = state.score();
|
||||
match state.mode {
|
||||
ScoringMode::SharedGround => o.total as i32,
|
||||
ScoringMode::CommonProblem => o.personal.get(&seat).copied().unwrap_or(0),
|
||||
ScoringMode::BondedCoalitions => o
|
||||
.coalitions
|
||||
.iter()
|
||||
.find(|c| c.members.contains(&seat))
|
||||
.map(|c| c.score)
|
||||
// A seat in no coalition is a one-seat coalition (GR-E04), so
|
||||
// falling back to its own score is the rule and not a guess.
|
||||
.unwrap_or_else(|| o.personal.get(&seat).copied().unwrap_or(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A seat that plays **its own** objective (CB-WP-0049 T03).
|
||||
///
|
||||
/// ## Where the modes actually differ, and where they cannot
|
||||
///
|
||||
/// Working this out was most of the task. **SOLVE always claims for the
|
||||
/// actor**, so a seat maximising its own score and a seat maximising the
|
||||
/// group's want the same SOLVE in almost every position — which is a
|
||||
/// fact about GROUND's action set, not a shortcoming of the bot, and it
|
||||
/// bounds how far apart any two policies can get.
|
||||
///
|
||||
/// Two places the objective really does diverge, both readable off the
|
||||
/// table:
|
||||
///
|
||||
/// - **SUPPORT regulates someone else.** Under SHARED GROUND that is
|
||||
/// worth what it is worth to the group. Under COMMON PROBLEM the
|
||||
/// beneficiary is a rival, so it is worth less. Under BONDED
|
||||
/// COALITIONS a Bond *merges that seat into my coalition*, and my score
|
||||
/// becomes the coalition's sum — so it is worth more, and worth most
|
||||
/// with a seat not already in my network.
|
||||
/// - **SOLVE's value is the Problem's value.** `GreedyPolicy::rank` gives
|
||||
/// every legal SOLVE 90 regardless of what the card is worth. Under a
|
||||
/// competitive objective the difference between a 2 and a 3 is the
|
||||
/// whole margin.
|
||||
///
|
||||
/// **It delegates.** Every arm this does not name comes from
|
||||
/// `GreedyPolicy::rank`. CB-WP-0039 re-typed an abridged copy of greedy
|
||||
/// and called it "one preference changed"; it differed in five places and
|
||||
/// burned the Freedom token in round one of every game. Delegating makes
|
||||
/// "these arms and no others" structurally true instead of a claim in a
|
||||
/// comment.
|
||||
///
|
||||
/// **Blind by construction** (ADR-0023): it reads Problem values only for
|
||||
/// face-up Problems, and relations, which are on the table.
|
||||
pub struct ObjectivePolicy;
|
||||
|
||||
impl ObjectivePolicy {
|
||||
/// The seats whose personal score counts toward `seat`'s objective.
|
||||
fn my_side(state: &GroundState, seat: PlayerId) -> Vec<PlayerId> {
|
||||
match state.mode {
|
||||
// Everyone's claims are my claims.
|
||||
ScoringMode::SharedGround => state.players.keys().copied().collect(),
|
||||
ScoringMode::CommonProblem => vec![seat],
|
||||
ScoringMode::BondedCoalitions => state
|
||||
.score()
|
||||
.coalitions
|
||||
.into_iter()
|
||||
.find(|c| c.members.contains(&seat))
|
||||
.map(|c| c.members)
|
||||
.unwrap_or_else(|| vec![seat]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 {
|
||||
let base = GreedyPolicy::rank(state, seat, cmd);
|
||||
match cmd {
|
||||
// SOLVE, weighted by what the card is worth. Face-up only:
|
||||
// `legal_commands` offers SOLVE on face-up Problems, and the
|
||||
// lookup returns nothing for a card this seat cannot read.
|
||||
GroundCommand::SelectAction {
|
||||
action: Action::Solve,
|
||||
problem: Some(n),
|
||||
..
|
||||
} => {
|
||||
let worth = state
|
||||
.problems
|
||||
.get(n)
|
||||
.filter(|p| p.face_up && p.claimed_by.is_none())
|
||||
.map(|p| i32::from(p.value))
|
||||
.unwrap_or(0);
|
||||
base + worth
|
||||
}
|
||||
// SUPPORT, weighted by whether the target is on my side.
|
||||
GroundCommand::SelectAction {
|
||||
action: Action::Support,
|
||||
target: Some(other),
|
||||
..
|
||||
} => {
|
||||
let side = Self::my_side(state, seat);
|
||||
if side.contains(other) {
|
||||
base + 5
|
||||
} else {
|
||||
match state.mode {
|
||||
// A Bond would bring them onto my side, and my
|
||||
// score is my side's sum.
|
||||
ScoringMode::BondedCoalitions => base + 10,
|
||||
// Regulating a rival is work I do for them.
|
||||
ScoringMode::CommonProblem => base - 30,
|
||||
ScoringMode::SharedGround => base,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Policy for ObjectivePolicy {
|
||||
fn name(&self) -> &'static str {
|
||||
"objective"
|
||||
}
|
||||
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]).
|
||||
///
|
||||
/// `Policy::choose` takes the whole `GroundState`, which carries every
|
||||
|
|
@ -1272,6 +1421,135 @@ mod blindness_tests {
|
|||
assert!(err.contains("peeker"), "{err}");
|
||||
}
|
||||
|
||||
/// The new policy is blind too — the point of T01 being first.
|
||||
#[test]
|
||||
fn the_objective_policy_is_blind() {
|
||||
for seed in [1u64, 7, 42] {
|
||||
for players in [2u8, 4, 6] {
|
||||
for mode in [
|
||||
ScoringMode::SharedGround,
|
||||
ScoringMode::CommonProblem,
|
||||
ScoringMode::BondedCoalitions,
|
||||
] {
|
||||
let mut st = deal(players, seed);
|
||||
st.mode = mode;
|
||||
for seat in st.players.keys().copied() {
|
||||
is_blind(|| ObjectivePolicy, &st, seat)
|
||||
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **The objective is the game's own scoring** (CB-WP-0049 T02).
|
||||
#[test]
|
||||
fn the_objective_agrees_with_the_outcome() {
|
||||
let mut st = deal(4, 5);
|
||||
let seats: Vec<PlayerId> = st.players.keys().copied().collect();
|
||||
// Claim two Problems for two different seats, so group and
|
||||
// personal cannot coincide by accident.
|
||||
let keys: Vec<u32> = st.problems.keys().copied().collect();
|
||||
st.problems.get_mut(&keys[0]).unwrap().claimed_by = Some(seats[0]);
|
||||
st.problems.get_mut(&keys[1]).unwrap().claimed_by = Some(seats[1]);
|
||||
let o = st.score();
|
||||
|
||||
st.mode = ScoringMode::SharedGround;
|
||||
assert_eq!(objective(&st, seats[0]), o.total as i32);
|
||||
// Everyone shares one number.
|
||||
assert_eq!(objective(&st, seats[0]), objective(&st, seats[2]));
|
||||
|
||||
st.mode = ScoringMode::CommonProblem;
|
||||
assert_eq!(objective(&st, seats[0]), o.personal[&seats[0]]);
|
||||
// **And a seat's objective is NOT the group's** — the claim that
|
||||
// makes a competitive mode competitive, asserted on a board
|
||||
// rather than argued.
|
||||
assert_ne!(
|
||||
objective(&st, seats[0]),
|
||||
st.score().total as i32,
|
||||
"under COMMON PROBLEM a seat's objective coincided with the group's"
|
||||
);
|
||||
assert_ne!(
|
||||
objective(&st, seats[0]),
|
||||
objective(&st, seats[2]),
|
||||
"a claiming seat and an empty-handed seat had the same objective"
|
||||
);
|
||||
|
||||
st.mode = ScoringMode::BondedCoalitions;
|
||||
let mine = st
|
||||
.score()
|
||||
.coalitions
|
||||
.into_iter()
|
||||
.find(|c| c.members.contains(&seats[0]))
|
||||
.expect("every seat is in a coalition");
|
||||
assert_eq!(objective(&st, seats[0]), mine.score);
|
||||
}
|
||||
|
||||
/// **Under SHARED GROUND the objective policy IS greedy** — the
|
||||
/// control that separates "attends to the objective" from "plays
|
||||
/// differently".
|
||||
///
|
||||
/// Without it, any change in the panel could be the new policy simply
|
||||
/// being a different bot. The two must agree wherever the objective
|
||||
/// is the group's, and diverge only where it is not.
|
||||
#[test]
|
||||
fn under_shared_ground_the_objective_policy_and_greedy_want_the_same_thing() {
|
||||
let mut differed = 0;
|
||||
for seed in [1u64, 7, 42, 99] {
|
||||
for players in [2u8, 4, 6] {
|
||||
let mut st = deal(players, seed);
|
||||
st.mode = ScoringMode::SharedGround;
|
||||
for seat in st.players.keys().copied() {
|
||||
let legal = legal_commands(&st, seat);
|
||||
if legal.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let a = GreedyPolicy.choose(&st, seat, &legal, false);
|
||||
let b = ObjectivePolicy.choose(&st, seat, &legal, false);
|
||||
if a != b {
|
||||
differed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// SOLVE is weighted by card value in every mode, so a tie greedy
|
||||
// broke by order can break the other way here. That is a
|
||||
// refinement of greedy's own objective, not a different one — so
|
||||
// the assertion is that divergence is RARE, with the number
|
||||
// stated rather than a vague "mostly".
|
||||
assert!(
|
||||
differed <= 2,
|
||||
"under SHARED GROUND the two policies differed at {differed} decision points; they are supposed to share an objective"
|
||||
);
|
||||
}
|
||||
|
||||
/// **Under COMMON PROBLEM they do NOT** — and the reason is Support.
|
||||
#[test]
|
||||
fn a_competitive_seat_values_supporting_a_rival_less() {
|
||||
let mut st = deal(4, 5);
|
||||
let seat = PlayerId(0);
|
||||
let other = PlayerId(1);
|
||||
let support = GroundCommand::SelectAction {
|
||||
action: Action::Support,
|
||||
target: Some(other),
|
||||
problem: None,
|
||||
};
|
||||
st.mode = ScoringMode::SharedGround;
|
||||
let shared = ObjectivePolicy::rank(&st, seat, &support);
|
||||
st.mode = ScoringMode::CommonProblem;
|
||||
let selfish = ObjectivePolicy::rank(&st, seat, &support);
|
||||
st.mode = ScoringMode::BondedCoalitions;
|
||||
let coalition = ObjectivePolicy::rank(&st, seat, &support);
|
||||
assert!(
|
||||
selfish < shared,
|
||||
"a seat scoring only its own claims valued regulating a rival the same as a cooperative seat did ({selfish} vs {shared})"
|
||||
);
|
||||
assert!(
|
||||
coalition > shared,
|
||||
"under BONDED COALITIONS a Bond brings that seat's score into mine, so Support is worth MORE, not the same ({coalition} vs {shared})"
|
||||
);
|
||||
}
|
||||
|
||||
/// And the shipped policies are blind.
|
||||
#[test]
|
||||
fn every_shipped_policy_is_blind_to_what_its_seat_cannot_see() {
|
||||
|
|
|
|||
|
|
@ -283,6 +283,29 @@ fn no_kernel_path(cat: &crate::catalog::Catalog, id: &str) -> String {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A module the catalog has and this kernel has **no path for**.
|
||||
///
|
||||
/// **Found by asking `resolve`, not by matching a status string.**
|
||||
/// The first version searched for `status: proposed` — and
|
||||
/// ground-game renamed those to `ready-for-implement` the same day,
|
||||
/// so two tests failed over a word neither of them was about. The
|
||||
/// structural property is ours to determine and does not move when
|
||||
/// another repo edits its vocabulary; the same lesson as matching a
|
||||
/// rules passage by heading rather than by row number (CB-WP-0046).
|
||||
///
|
||||
/// `None` when the kernel implements everything — a real state, and
|
||||
/// the day it arrives these tests should skip rather than fail.
|
||||
fn a_module_we_cannot_run() -> Option<(String, String)> {
|
||||
let cat = crate::catalog::catalog().ok()?;
|
||||
cat.modules.iter().find_map(|m| {
|
||||
let mut c = Configuration::default();
|
||||
c.modules.insert(m.aspect.clone(), m.module_id.clone());
|
||||
c.resolve()
|
||||
.is_err()
|
||||
.then(|| (m.aspect.clone(), m.module_id.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
/// **A proposed module is refused BY NAME, distinguishably from a
|
||||
/// typo** (ADR-0022 D1).
|
||||
///
|
||||
|
|
@ -292,23 +315,16 @@ mod tests {
|
|||
/// statement about the edition, and the wrong-subject family again.
|
||||
#[test]
|
||||
fn a_proposed_module_and_a_typo_are_different_errors() {
|
||||
let cat = crate::catalog::catalog().expect("catalog.yaml");
|
||||
// A module the catalog has, with a rules_delta, that no kernel
|
||||
// path implements. Found in the catalog, not hardcoded: if
|
||||
// ground-game implements it upstream this test looks elsewhere
|
||||
// rather than going stale.
|
||||
let proposed = cat
|
||||
.modules
|
||||
.iter()
|
||||
.find(|m| m.status == "proposed")
|
||||
.expect("the catalog has at least one proposed module");
|
||||
|
||||
let Some((aspect, module)) = a_module_we_cannot_run() else {
|
||||
// The kernel implements every module the catalog has. Nothing
|
||||
// to distinguish, and nothing broken.
|
||||
return;
|
||||
};
|
||||
let mut c = Configuration::default();
|
||||
c.modules
|
||||
.insert(proposed.aspect.clone(), proposed.module_id.clone());
|
||||
c.modules.insert(aspect.clone(), module.clone());
|
||||
let refused = c.resolve().expect_err("a proposed module must be refused");
|
||||
assert!(
|
||||
refused.contains(&proposed.module_id) && refused.contains("no kernel path"),
|
||||
refused.contains(&module) && refused.contains("no kernel path"),
|
||||
"a proposed module was not refused by name: {refused}"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -318,10 +334,7 @@ mod tests {
|
|||
|
||||
// And a real typo says the other thing.
|
||||
let mut typo = Configuration::default();
|
||||
typo.modules.insert(
|
||||
proposed.aspect.clone(),
|
||||
format!("{}_zzz", proposed.module_id),
|
||||
);
|
||||
typo.modules.insert(aspect, format!("{module}_zzz"));
|
||||
let unknown = typo.resolve().expect_err("a typo must be refused");
|
||||
assert!(
|
||||
unknown.contains("is not a module the catalog has"),
|
||||
|
|
@ -358,21 +371,12 @@ mod tests {
|
|||
/// failure (ADR-0022 D0).
|
||||
#[test]
|
||||
fn a_module_with_no_kernel_path_still_names_a_configuration() {
|
||||
let cat = crate::catalog::catalog().expect("catalog.yaml");
|
||||
let proposed = cat
|
||||
.modules
|
||||
.iter()
|
||||
.find(|m| m.status == "proposed")
|
||||
.expect("a proposed module");
|
||||
let c = Configuration::from_modules(
|
||||
"ground-darvo-r0",
|
||||
std::slice::from_ref(&proposed.module_id),
|
||||
)
|
||||
.expect("selecting a catalog module must NAME a configuration");
|
||||
assert_eq!(
|
||||
c.module_on(&proposed.aspect),
|
||||
Some(proposed.module_id.as_str())
|
||||
);
|
||||
let Some((aspect, module)) = a_module_we_cannot_run() else {
|
||||
return;
|
||||
};
|
||||
let c = Configuration::from_modules("ground-darvo-r0", std::slice::from_ref(&module))
|
||||
.expect("selecting a catalog module must NAME a configuration");
|
||||
assert_eq!(c.module_on(&aspect), Some(module.as_str()));
|
||||
// It round-trips, so a recording can carry it.
|
||||
let s = serde_yaml::to_string(&c).unwrap();
|
||||
let back: Configuration = serde_yaml::from_str(&s).unwrap();
|
||||
|
|
|
|||
|
|
@ -1834,7 +1834,15 @@ impl GroundState {
|
|||
}
|
||||
|
||||
/// GR-E01..E04: final scoring for the configured mode.
|
||||
fn score(&self) -> Outcome {
|
||||
/// **Public since CB-WP-0049 T02.** A policy needs to know what its
|
||||
/// seat is trying to maximise, and the game already computes all
|
||||
/// three answers here. A second copy in the bot would be a second
|
||||
/// definition of winning — and the two would disagree the first time
|
||||
/// ground-game rules on F28's mastery reading.
|
||||
///
|
||||
/// It is a pure function of the state, so it answers mid-game too:
|
||||
/// "what would this position score if it stopped now".
|
||||
pub fn score(&self) -> Outcome {
|
||||
// GR-E01/P03: a claimed Problem counts its printed value.
|
||||
let total: u32 = self
|
||||
.problems
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue