fix(reference): Death Events remember a right-hand neighbour, not a board index
A Deathrattle summon used the dying minion's board index from before the Death Creation Step removed anything. That index is stale when a minion to its left dies in the same step, so the token landed one place too far right. Each Death Event now records `before`: the nearest minion to the right that is not dying in the same step. The position handed to the effect context is resolved when the trigger resolves: the current index of `before`, or the right end. Applied to the Rust and TS references of Modules 3-10. Module 3 gains two tests, documented in scenarios.md: test_03_summon_position_after_simultaneous_deaths (fails before the fix) and test_03_simultaneous_summons_keep_board_order. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
638549161f
commit
429137a6ef
79 files changed
+808
-236
No files matched your search
@@ -5,26 +5,37 @@
|
||||
//! ends. Then this step removes every marked minion at once.
|
||||
|
||||
use super::event::Event;
|
||||
use super::world::World;
|
||||
use super::world::{EntityId, World, Zone};
|
||||
|
||||
/// Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
/// Returns their Death Events in order of play; empty if nothing died.
|
||||
pub fn death_creation_step(world: &mut World) -> Vec<Event> {
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
// Every neighbour is read before anything moves: removal is simultaneous. Each Death
|
||||
// Event remembers where its minion stood as a handle, the minion that stays to its
|
||||
// right, not as a board index: an index goes stale as soon as a minion further left
|
||||
// leaves play in the same step (Module 1: hold handles, not indexes).
|
||||
let mut deaths = Vec::new();
|
||||
for id in world.minions_in_play() {
|
||||
if world.is_doomed(id) {
|
||||
let position = world
|
||||
.board_index(id)
|
||||
.expect("a minion in play is on the board");
|
||||
deaths.push(Event::Destroyed { id, position });
|
||||
let before = survivor_to_the_right(world, id);
|
||||
deaths.push(Event::Destroyed { id, before });
|
||||
}
|
||||
}
|
||||
for death in &deaths {
|
||||
if let Event::Destroyed { id, position } = *death {
|
||||
world.kill(id, position);
|
||||
if let Event::Destroyed { id, before } = *death {
|
||||
world.kill(id, before);
|
||||
}
|
||||
}
|
||||
deaths
|
||||
}
|
||||
|
||||
/// The nearest minion to the right of `id` on its board that is not leaving play in this step, if any.
|
||||
fn survivor_to_the_right(world: &World, id: EntityId) -> Option<EntityId> {
|
||||
world
|
||||
.zone(world.owner_of(id), Zone::Board)
|
||||
.iter()
|
||||
.copied()
|
||||
.skip_while(|&other| other != id)
|
||||
.skip(1)
|
||||
.find(|&other| !world.is_doomed(other))
|
||||
}
|
||||
@@ -36,10 +36,12 @@ pub enum Event {
|
||||
card: String,
|
||||
},
|
||||
/// A Death Event: the Death Creation Step moved `id` from play to the graveyard.
|
||||
/// `position` is the board index it had when the step began; its Deathrattle summons there.
|
||||
/// `before` is the nearest minion to its right that did not die in the same step, if any:
|
||||
/// its Deathrattle summons immediately left of that minion, or at the right end. It is a
|
||||
/// handle because a board index goes stale when a minion further left dies in the same step.
|
||||
Destroyed {
|
||||
id: EntityId,
|
||||
position: usize,
|
||||
before: Option<EntityId>,
|
||||
},
|
||||
DeathPhase {
|
||||
round: u32,
|
||||
|
||||
@@ -59,8 +59,8 @@ pub struct Ctx<'a> {
|
||||
pub source: Option<EntityId>,
|
||||
/// The chosen target, if any.
|
||||
pub target: Option<EntityId>,
|
||||
/// The source's board position. For a Deathrattle, the source has left play,
|
||||
/// and this is the position its Death Event remembered.
|
||||
/// The source's board position. For a Deathrattle, the source has left play, and
|
||||
/// this is where it stood: immediately left of the minion its Death Event remembers.
|
||||
pub position: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -167,7 +167,9 @@ fn react(world: &mut World, events: &[Event]) {
|
||||
on: trigger.on,
|
||||
});
|
||||
let position = match *event {
|
||||
Event::Destroyed { id, position } if id == source => Some(position),
|
||||
Event::Destroyed { id, before } if id == source => {
|
||||
Some(position_left_of(world, world.owner_of(source), before))
|
||||
}
|
||||
_ => world.board_index(source),
|
||||
};
|
||||
let ctx = Ctx {
|
||||
@@ -185,6 +187,16 @@ fn react(world: &mut World, events: &[Event]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
/// right end if `before` is `None` or no longer there. A Death Event remembers the
|
||||
/// minion, not an index, so this still holds after other minions have left or arrived.
|
||||
fn position_left_of(world: &World, owner: PlayerId, before: Option<EntityId>) -> usize {
|
||||
let board = world.zone(owner, Zone::Board);
|
||||
before
|
||||
.and_then(|before| board.iter().position(|&id| id == before))
|
||||
.unwrap_or(board.len())
|
||||
}
|
||||
|
||||
/// Who may respond to `event`, in order of play: every minion in play and, for a
|
||||
/// Death Event, the minion that died. It has left play, but its Deathrattle still answers.
|
||||
fn listeners(world: &World, event: &Event) -> Vec<EntityId> {
|
||||
|
||||
@@ -276,10 +276,10 @@ impl World {
|
||||
}
|
||||
|
||||
/// Move a minion from play to its owner's graveyard, recording its Death Event.
|
||||
pub fn kill(&mut self, id: EntityId, position: usize) {
|
||||
pub fn kill(&mut self, id: EntityId, before: Option<EntityId>) {
|
||||
self.entity_mut(id).pending_destroy = false;
|
||||
self.move_to(id, Zone::Graveyard);
|
||||
self.record(Event::Destroyed { id, position });
|
||||
self.record(Event::Destroyed { id, before });
|
||||
}
|
||||
|
||||
pub fn gain_armor(&mut self, player: PlayerId, amount: i32) {
|
||||
|
||||
Reference in new issue
Block a user