feat: log tree from Module 2 (logTree / log_tree); SpellGrid CLIs print Log:
Engine events carry the nesting depth they were recorded at; the harness prints the log as an indented tree. New tests test_02_log_tree_shows_nesting and test_03_log_tree_death_phase; both SpellGrid CLIs print the tree of the last command's Sequences under a Log: line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
06a1b34c5d
commit
97c64ebee8
86 files changed
+1375
-55
No files matched your search
@@ -1,7 +1,8 @@
|
||||
//! SpellGrid on the command line: `cargo run --bin spellgrid -- levels/l1-double-tap.json`.
|
||||
//!
|
||||
//! A thin loop over the harness: it draws the level, reads a command, and calls
|
||||
//! `step`, `cast` or `play`. No rules live here.
|
||||
//! `step`, `cast` or `play`. No rules live here. After a command that ran Part I
|
||||
//! Sequences, it prints their events under `Log:` as an indented tree.
|
||||
//!
|
||||
//! ```text
|
||||
//! w a s d one turn: up, left, down, right
|
||||
@@ -159,6 +160,8 @@ fn play(g: &mut SpellGrid, level: &LevelDef, io: &Rc<RefCell<Io>>) -> Outcome {
|
||||
return Outcome::Quit;
|
||||
};
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
// The log holds only what this command does.
|
||||
g.clear_log();
|
||||
let dir = match words.first() {
|
||||
Some(&"q") => return Outcome::Quit,
|
||||
Some(&"w") => Some(Dir::Up),
|
||||
@@ -189,5 +192,9 @@ fn play(g: &mut SpellGrid, level: &LevelDef, io: &Rc<RefCell<Io>>) -> Outcome {
|
||||
if let Err(message) = done {
|
||||
io.borrow_mut().write(&format!("{message}\n"));
|
||||
}
|
||||
let tree = g.log_tree();
|
||||
if !tree.is_empty() {
|
||||
io.borrow_mut().write(&format!("Log:\n{tree}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! What happened, in order. The engine records an event each time it changes the game.
|
||||
//! The world also keeps the nesting depth each event was recorded at (`World::log_depths`).
|
||||
|
||||
use super::card::TriggerOn;
|
||||
use super::world::{EntityId, PlayerId};
|
||||
|
||||
@@ -275,8 +275,11 @@ fn react(world: &mut World, events: &[Event]) {
|
||||
}
|
||||
_ => world.board_index(source),
|
||||
};
|
||||
// The trigger's consequences are one level deeper in the log tree.
|
||||
world.open_block();
|
||||
let owner = world.owner_of(source);
|
||||
run_effects(world, &trigger.effects, owner, Some(source), None, position);
|
||||
world.close_block();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@ pub struct World {
|
||||
sides: [Side; 2],
|
||||
next_order_of_play: u32,
|
||||
log: Vec<Event>,
|
||||
/// The nesting depth at which each event in `log` was recorded, index for index.
|
||||
depths: Vec<u32>,
|
||||
/// How many triggers are resolving right now, one inside another.
|
||||
depth: u32,
|
||||
/// The auras in force as of the last Aura Update. Changes only there (see `aura.rs`).
|
||||
auras: Vec<ActiveAura>,
|
||||
/// Module 10: auras that no minion carries, for every player's minions. SpellGrid
|
||||
@@ -105,6 +109,8 @@ impl World {
|
||||
sides: Default::default(),
|
||||
next_order_of_play: 0,
|
||||
log: Vec::new(),
|
||||
depths: Vec::new(),
|
||||
depth: 0,
|
||||
auras: Vec::new(),
|
||||
rule_auras: Vec::new(),
|
||||
end_of_phase: END_OF_PHASE.to_vec(),
|
||||
@@ -310,10 +316,27 @@ impl World {
|
||||
&self.log
|
||||
}
|
||||
|
||||
/// The nesting depth of each event in `log()`, index for index: 0 for the outermost
|
||||
/// action's own events.
|
||||
pub fn log_depths(&self) -> &[u32] {
|
||||
&self.depths
|
||||
}
|
||||
|
||||
// ---- Changes: each one records its event ----------------------------
|
||||
|
||||
pub fn record(&mut self, event: Event) {
|
||||
self.log.push(event);
|
||||
self.depths.push(self.depth);
|
||||
}
|
||||
|
||||
/// A trigger starts resolving: the events it causes are recorded one level deeper.
|
||||
pub fn open_block(&mut self) {
|
||||
self.depth += 1;
|
||||
}
|
||||
|
||||
/// The trigger that last opened a block has finished resolving.
|
||||
pub fn close_block(&mut self) {
|
||||
self.depth -= 1;
|
||||
}
|
||||
|
||||
/// Add a card to the library, or replace the card with the same name.
|
||||
@@ -358,6 +381,7 @@ impl World {
|
||||
|
||||
pub fn clear_log(&mut self) {
|
||||
self.log.clear();
|
||||
self.depths.clear();
|
||||
}
|
||||
|
||||
pub fn damage(&mut self, target: EntityId, amount: i32) {
|
||||
|
||||
@@ -60,6 +60,48 @@ impl GameEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// One event as `scenarios.md` writes it, such as `Triggered{source: 3, trigger: "deathrattle"}`.
|
||||
impl std::fmt::Display for GameEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
GameEvent::Summoned { id } => write!(f, "Summoned{{id: {id}}}"),
|
||||
GameEvent::Damaged { target, amount } => {
|
||||
write!(f, "Damaged{{target: {target}, amount: {amount}}}")
|
||||
}
|
||||
GameEvent::Triggered { source, trigger } => {
|
||||
write!(f, "Triggered{{source: {source}, trigger: {trigger:?}}}")
|
||||
}
|
||||
GameEvent::Destroyed { id } => write!(f, "Destroyed{{id: {id}}}"),
|
||||
GameEvent::DeathPhase { round } => write!(f, "DeathPhase{{round: {round}}}"),
|
||||
GameEvent::Drew { player, id } => write!(f, "Drew{{player: {player}, id: {id}}}"),
|
||||
GameEvent::Fatigue { player } => write!(f, "Fatigue{{player: {player}}}"),
|
||||
GameEvent::ArmorGained { player, amount } => {
|
||||
write!(f, "ArmorGained{{player: {player}, amount: {amount}}}")
|
||||
}
|
||||
GameEvent::StatsChanged { id, attack, health } => {
|
||||
write!(
|
||||
f,
|
||||
"StatsChanged{{id: {id}, attack: {attack}, health: {health}}}"
|
||||
)
|
||||
}
|
||||
GameEvent::SpellCast { card } => write!(f, "SpellCast{{card: {card:?}}}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The log as a tree: one event per line, two spaces of indent per level of nesting.
|
||||
fn log_tree(world: &World, view: impl Fn(&Event) -> Option<GameEvent>) -> String {
|
||||
world
|
||||
.log()
|
||||
.iter()
|
||||
.zip(world.log_depths())
|
||||
.filter_map(|(event, &depth)| {
|
||||
let shown = view(event)?;
|
||||
Some(format!("{}{shown}\n", " ".repeat(depth as usize)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
world: World,
|
||||
}
|
||||
@@ -151,6 +193,10 @@ impl Game {
|
||||
pub fn clear_log(&mut self) {
|
||||
self.world.clear_log();
|
||||
}
|
||||
/// The log as an indented tree (see course/harness.md).
|
||||
pub fn log_tree(&self) -> String {
|
||||
log_tree(&self.world, GameEvent::from_engine)
|
||||
}
|
||||
|
||||
// ---- Module 3 ----
|
||||
|
||||
@@ -396,42 +442,45 @@ impl SpellGrid {
|
||||
.map(|&id| world.card_name(id).to_string())
|
||||
.collect()
|
||||
}
|
||||
/// The course's view of an engine event, with every entity id as a grid id.
|
||||
fn grid_event(&self, event: &Event) -> Option<GameEvent> {
|
||||
let id = |entity: Id| self.grid.id_of(entity);
|
||||
Some(match GameEvent::from_engine(event)? {
|
||||
GameEvent::Summoned { id: e } => GameEvent::Summoned { id: id(e) },
|
||||
GameEvent::Destroyed { id: e } => GameEvent::Destroyed { id: id(e) },
|
||||
GameEvent::Drew { player, id: e } => GameEvent::Drew { player, id: id(e) },
|
||||
GameEvent::StatsChanged {
|
||||
id: e,
|
||||
attack,
|
||||
health,
|
||||
} => GameEvent::StatsChanged {
|
||||
id: id(e),
|
||||
attack,
|
||||
health,
|
||||
},
|
||||
GameEvent::Damaged { target, amount } => GameEvent::Damaged {
|
||||
target: id(target),
|
||||
amount,
|
||||
},
|
||||
GameEvent::Triggered { source, trigger } => GameEvent::Triggered {
|
||||
source: id(source),
|
||||
trigger,
|
||||
},
|
||||
other => other,
|
||||
})
|
||||
}
|
||||
/// Part I's log, with grid ids.
|
||||
pub fn log(&self) -> Vec<GameEvent> {
|
||||
let id = |entity: Id| self.grid.id_of(entity);
|
||||
self.grid
|
||||
.world()
|
||||
.log()
|
||||
.iter()
|
||||
.filter_map(GameEvent::from_engine)
|
||||
.map(|event| match event {
|
||||
GameEvent::Summoned { id: e } => GameEvent::Summoned { id: id(e) },
|
||||
GameEvent::Destroyed { id: e } => GameEvent::Destroyed { id: id(e) },
|
||||
GameEvent::Drew { player, id: e } => GameEvent::Drew { player, id: id(e) },
|
||||
GameEvent::StatsChanged {
|
||||
id: e,
|
||||
attack,
|
||||
health,
|
||||
} => GameEvent::StatsChanged {
|
||||
id: id(e),
|
||||
attack,
|
||||
health,
|
||||
},
|
||||
GameEvent::Damaged { target, amount } => GameEvent::Damaged {
|
||||
target: id(target),
|
||||
amount,
|
||||
},
|
||||
GameEvent::Triggered { source, trigger } => GameEvent::Triggered {
|
||||
source: id(source),
|
||||
trigger,
|
||||
},
|
||||
other => other,
|
||||
})
|
||||
.collect()
|
||||
let log = self.grid.world().log();
|
||||
log.iter().filter_map(|e| self.grid_event(e)).collect()
|
||||
}
|
||||
pub fn clear_log(&mut self) {
|
||||
self.grid.world_mut().clear_log();
|
||||
}
|
||||
/// Part I's log as an indented tree, with grid ids (see course/harness.md).
|
||||
pub fn log_tree(&self) -> String {
|
||||
log_tree(self.grid.world(), |e| self.grid_event(e))
|
||||
}
|
||||
/// Who makes Assemble's choices (`"base"`, `"donor"`, `"half"`).
|
||||
pub fn set_chooser(&mut self, chooser: impl FnMut(&str, &[String]) -> String + 'static) {
|
||||
self.grid.world_mut().set_chooser(Box::new(chooser));
|
||||
|
||||
Reference in new issue
Block a user