CB-WP-0018-T02: a game log the player can read
bot::Journal -- a shared list of Applied { actor, command, events } the
driver appends to via play_journaled; play delegates with None so
nothing existing changed. BotGame.events only appears after play
returns, which is no use to a page rendered mid-game.
Phrased with record::to_step, the recorder's vocabulary, so what the
player reads is what the scenario file will say, and all 29 GroundEvent
variants now render in words instead of Debug.
A command that produced no events says 'no effect'; the mutation
dropping that branch goes red. Honest limitation recorded: the reported
SOLVE case is resolved inside the system's resolve command, which does
produce events for other seats, so it shows as a selection with no claim
following rather than an explicit 'no effect'. Making it explicit would
mean the renderer deciding why a rule did nothing -- a second
implementation of the rules, which this task's control forbids.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
57639623da
commit
7a78c58404
6 changed files with 333 additions and 15 deletions
|
|
@ -409,10 +409,23 @@ const MAX_ATTEMPTS: usize = 8;
|
|||
/// Seats are matched to `policies` by index: `PlayerId(n)` gets
|
||||
/// `policies[n]`.
|
||||
pub fn play<'a>(
|
||||
mut state: GroundState,
|
||||
state: GroundState,
|
||||
policies: &mut [Box<dyn Policy + 'a>],
|
||||
) -> Result<BotGame, BotError> {
|
||||
let mut log = Log::default();
|
||||
play_journaled(state, policies, None)
|
||||
}
|
||||
|
||||
/// The same, appending every applied command and its events to `journal`
|
||||
/// as it goes, for a caller rendering the game while it runs.
|
||||
pub fn play_journaled<'a>(
|
||||
mut state: GroundState,
|
||||
policies: &mut [Box<dyn Policy + 'a>],
|
||||
journal: Option<Journal>,
|
||||
) -> Result<BotGame, BotError> {
|
||||
let mut log = Log {
|
||||
journal,
|
||||
..Log::default()
|
||||
};
|
||||
|
||||
let seats: Vec<PlayerId> = state.players.keys().copied().collect();
|
||||
for seat in &seats {
|
||||
|
|
@ -504,11 +517,33 @@ pub fn play<'a>(
|
|||
})
|
||||
}
|
||||
|
||||
/// One command and everything it produced.
|
||||
///
|
||||
/// **The empty case is the load-bearing one** (CB-WP-0018 T02). GR-A02's
|
||||
/// resolver silently `continue`s when a SOLVE cannot be fulfilled, so a
|
||||
/// player can select it three rounds running and change nothing. A journal
|
||||
/// built only from events would show nothing for those and reproduce the
|
||||
/// silence; keeping the command with an empty `events` is what lets a
|
||||
/// reader say *"this happened and did nothing"*.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Applied {
|
||||
pub actor: Actor,
|
||||
pub command: GroundCommand,
|
||||
pub events: Vec<crate::GroundEvent>,
|
||||
}
|
||||
|
||||
/// A live account of a game in progress, shared with whoever is watching.
|
||||
///
|
||||
/// `BotGame.events` is the same information but only after `play` returns,
|
||||
/// which is no use to a page rendered mid-game.
|
||||
pub type Journal = std::rc::Rc<std::cell::RefCell<Vec<Applied>>>;
|
||||
|
||||
/// What the driver accumulates while a game runs.
|
||||
#[derive(Default)]
|
||||
struct Log {
|
||||
events: Vec<crate::GroundEvent>,
|
||||
steps: Vec<(Actor, GroundCommand)>,
|
||||
journal: Option<Journal>,
|
||||
}
|
||||
|
||||
/// Offer one seat its legal commands and apply what the policy picks.
|
||||
|
|
@ -573,6 +608,13 @@ fn apply(
|
|||
for event in &produced {
|
||||
state.fold(event);
|
||||
}
|
||||
if let Some(j) = &log.journal {
|
||||
j.borrow_mut().push(Applied {
|
||||
actor,
|
||||
command: cmd.clone(),
|
||||
events: produced.clone(),
|
||||
});
|
||||
}
|
||||
log.events.extend(produced);
|
||||
log.steps.push((actor, cmd.clone()));
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue