CB-WP-0011-T01: the inspector shows everything, and a gate says so

The renderer moves out of the play loop into inspect.rs and grows from
24 to 42 of the 43 leaf paths a populated GroundView carries. What it
had been dropping was the whole DARVO state machine, the whole GROUND
practice, the scoring mode, Focus tokens, the discard pile, per-seat
protection, and every part of the outcome except the headline.

The load-bearing half is every_view_field_is_classified, which walks
the serialized view for leaf paths and requires each to be listed as
rendered (with a token the output must contain) or omitted (with a
reason). Paths rather than keys: 'problem' occurs under a DARVO target,
a GROUND choice and a Selection, and a key-set walk would let one of
the three vouch for the other two.

Four M-D1-MUT controls, each red for its stated reason. The
unclassified-field control fired for real on the first run --
players.*.hand, a field the gate's own author had missed.
This commit is contained in:
tegwick 2026-08-02 02:38:38 +02:00
parent c1d140626e
commit d2ca1046c0
5 changed files with 624 additions and 125 deletions

View file

@ -16,8 +16,9 @@ use cb_game_runtime::{Project, Setup, Viewer};
use cb_kernel::{Actor, PlayerId};
use games_ground::bot::{BotError, Choice, GreedyPolicy, Policy, RandomPolicy};
use games_ground::record::to_step;
use games_ground::view::{GroundView, PlayerView, ProblemView, SelectionView};
use games_ground::{GroundCommand, GroundState};
use crate::inspect::{render, seat_name};
use std::io::{BufRead, Write};
pub struct Config {
@ -58,19 +59,11 @@ pub struct Summary {
}
// ------------------------------------------------------------- rendering
fn suit_name(s: games_ground::Suit) -> &'static str {
match s {
games_ground::Suit::Clarify => "Clarify",
games_ground::Suit::Repair => "Repair",
games_ground::Suit::Boundary => "Boundary",
games_ground::Suit::Change => "Change",
}
}
fn seat_name(p: PlayerId) -> String {
format!("P{}", p.0 + 1)
}
//
// The table itself is rendered by `inspect`. What stays here is the
// vocabulary a *chooser* needs: how a legal command is described in the
// menu. Rendering the state and naming a move are different jobs, and
// T02 needs the first without the second.
fn describe(command: &GroundCommand) -> String {
// Reuse the recorder's vocabulary rather than inventing a third one:
@ -90,109 +83,6 @@ fn describe(command: &GroundCommand) -> String {
out
}
fn render_player(id: PlayerId, p: &PlayerView, is_viewer: bool) -> String {
let hand = match &p.hand {
Some(cards) => {
let names: Vec<&str> = cards.iter().map(|c| suit_name(c.suit)).collect();
format!("hand [{}]", names.join(", "))
}
None => format!("hand {} card(s)", p.hand_size),
};
format!(
" {}{} stress {} freedom {} darvo {:?} blame {} {hand}",
seat_name(id),
if is_viewer { " (you)" } else { " " },
p.stress,
if p.freedom_ready { "READY" } else { "SPENT" },
p.darvo,
p.blame_from.len(),
)
}
/// One seat's whole picture, from the projection and nothing else.
pub fn render(view: &GroundView) -> String {
let mut out = String::new();
out.push_str(&format!(
"\nround {} step {:?} lead {} deck {}\n",
view.round,
view.step,
seat_name(view.lead),
view.solution_deck_len,
));
for (id, p) in &view.players {
out.push_str(&render_player(*id, p, Some(*id) == view.viewer));
out.push('\n');
}
out.push_str(" problems:");
for (priority, problem) in &view.problems {
match problem {
ProblemView::FaceDown => out.push_str(&format!(" [{priority}] face-down")),
ProblemView::FaceUp {
suit,
value,
denied,
claimed_by,
..
} => {
out.push_str(&format!(
" [{priority}] {} {}{}{}",
suit_name(*suit),
value,
if *denied { " DENIED" } else { "" },
match claimed_by {
Some(p) => format!(" claimed by {}", seat_name(*p)),
None => String::new(),
}
));
}
}
}
out.push('\n');
if !view.relations.is_empty() {
out.push_str(" relations:");
for (pair, relation) in &view.relations {
out.push_str(&format!(" {pair} {relation:?}"));
}
out.push('\n');
}
if !view.selections.is_empty() {
out.push_str(" selections:");
for (id, sel) in &view.selections {
match sel {
SelectionView::Hidden => out.push_str(&format!(" {} face-down", seat_name(*id))),
SelectionView::Shown(s) => out.push_str(&format!(
" {} {:?}{}{}",
seat_name(*id),
s.action,
match s.target {
Some(t) => format!("{}", seat_name(t)),
None => String::new(),
},
match s.problem {
Some(p) => format!("→[{p}]"),
None => String::new(),
}
)),
}
}
out.push('\n');
}
if let Some(o) = &view.outcome {
out.push_str(&format!(
" OUTCOME total {} / threshold {} group {} winners {:?}\n",
o.total,
o.threshold,
if o.group_success {
"SUCCESS"
} else {
"failure"
},
o.winners.iter().map(|w| seat_name(*w)).collect::<Vec<_>>(),
));
}
out
}
// --------------------------------------------------------------- policies
/// A seat driven from stdin. Implements the same `Policy` the bots do, so