Merge branch 'worktree-agent-af31a2138071f5c19' into revise/edge-cases-and-sources
This commit is contained in:
commit
493a2af854
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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -14,7 +14,9 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number };
|
||||
@@ -38,8 +38,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -129,13 +129,27 @@ function react(world: World, events: Event[]): void {
|
||||
// The Queue is now locked: nothing that happens below adds to it.
|
||||
for (const [source, trigger, event] of queue) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
const actions = trigger.effect(new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
// Depth-first: each action, and every reaction it causes, finishes before the next.
|
||||
for (const action of actions) perform(world, action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -208,10 +208,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -54,3 +54,25 @@ A minion is removed from play only in the Death Creation Step, which runs when t
|
||||
- `health(G)` is 3: G was at full Health when it was removed.
|
||||
- `zoneOf(G)` is graveyard.
|
||||
- `nameOf(D)` is `"Damaged Golem"`.
|
||||
|
||||
### test_03_summon_position_after_simultaneous_deaths
|
||||
|
||||
- Setup: `newGame` with a deck of 5 `Test Filler` for player 0. Summon `Loot Hoarder` (id H), then `Harvest Golem` (id G), then `Test Reactor` (id R). `dealDamage(G, 2)`, which leaves G at 1 Health.
|
||||
- Action: player 0 casts `Whirlwind`.
|
||||
- Assertions:
|
||||
- `hand(0)` has 1 card; call it C. The log has exactly one `Summoned` event; call its id D.
|
||||
- The log is exactly, in this order: `SpellCast{card: "Whirlwind"}`, `Damaged{target: H, amount: 1}`, `Damaged{target: G, amount: 1}`, `Damaged{target: R, amount: 1}`, `Destroyed{id: H}`, `Destroyed{id: G}`, `DeathPhase{round: 1}`, `Triggered{source: H, trigger: "deathrattle"}`, `Drew{player: 0, id: C}`, `Triggered{source: G, trigger: "deathrattle"}`, `Summoned{id: D}`. H and G leave play in the same Death Creation Step.
|
||||
- `nameOf(D)` is `"Damaged Golem"`.
|
||||
- `board(0)` is `[D, R]`: the Damaged Golem stands where G stood, left of R. G was second from the left when the step began, but H, to its left, left play in the same step, so that board index no longer points at G's place.
|
||||
- `zoneOf(H)` and `zoneOf(G)` are graveyard.
|
||||
|
||||
### test_03_simultaneous_summons_keep_board_order
|
||||
|
||||
- Setup: an empty game. Summon `Harvest Golem` (id A), then another `Harvest Golem` (id B), then `Test Reactor` (id R). `dealDamage(A, 2)` and `dealDamage(B, 2)`, which leave both at 1 Health.
|
||||
- Action: player 0 casts `Whirlwind`.
|
||||
- Assertions:
|
||||
- The log has exactly two `Summoned` events; call their ids DA and DB, in log order.
|
||||
- The log is exactly, in this order: `SpellCast{card: "Whirlwind"}`, `Damaged{target: A, amount: 1}`, `Damaged{target: B, amount: 1}`, `Damaged{target: R, amount: 1}`, `Destroyed{id: A}`, `Destroyed{id: B}`, `DeathPhase{round: 1}`, `Triggered{source: A, trigger: "deathrattle"}`, `Summoned{id: DA}`, `Triggered{source: B, trigger: "deathrattle"}`, `Summoned{id: DB}`. A's Deathrattle resolves first, by order of play, so DA is A's Damaged Golem and DB is B's.
|
||||
- `nameOf(DA)` and `nameOf(DB)` are `"Damaged Golem"`.
|
||||
- `board(0)` is `[DA, DB, R]`: the two Damaged Golems stand in the order A and B stood, both left of R.
|
||||
- `zoneOf(A)` and `zoneOf(B)` are graveyard.
|
||||
@@ -22,6 +22,16 @@ fn whirlwind() -> GameEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the `Summoned` events in `log`, in order.
|
||||
fn summoned_ids(log: &[GameEvent]) -> Vec<Id> {
|
||||
log.iter()
|
||||
.filter_map(|e| match e {
|
||||
GameEvent::Summoned { id } => Some(*id),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_03_removed_only_after_sequence() {
|
||||
let mut g = new_game(GameOpts {
|
||||
@@ -155,3 +165,82 @@ fn test_03_destroy_marks_pending() {
|
||||
assert_eq!(g.zone_of(golem), Zone::Graveyard);
|
||||
assert_eq!(g.name_of(d), "Damaged Golem");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_03_summon_position_after_simultaneous_deaths() {
|
||||
let mut g = new_game(GameOpts {
|
||||
decks: Some([fillers(5), vec![]]),
|
||||
});
|
||||
let h = g.summon("Loot Hoarder", 0);
|
||||
let golem = g.summon("Harvest Golem", 0);
|
||||
let r = g.summon("Test Reactor", 0);
|
||||
g.deal_damage(golem, 2);
|
||||
g.clear_log();
|
||||
|
||||
g.cast("Whirlwind", 0, None);
|
||||
|
||||
let hand = g.hand(0);
|
||||
assert_eq!(hand.len(), 1);
|
||||
let c = hand[0];
|
||||
let summoned = summoned_ids(&g.log());
|
||||
assert_eq!(summoned.len(), 1);
|
||||
let d = summoned[0];
|
||||
assert_eq!(
|
||||
g.log(),
|
||||
vec![
|
||||
whirlwind(),
|
||||
damaged(h, 1),
|
||||
damaged(golem, 1),
|
||||
damaged(r, 1),
|
||||
GameEvent::Destroyed { id: h },
|
||||
GameEvent::Destroyed { id: golem },
|
||||
GameEvent::DeathPhase { round: 1 },
|
||||
triggered(h, "deathrattle"),
|
||||
GameEvent::Drew { player: 0, id: c },
|
||||
triggered(golem, "deathrattle"),
|
||||
GameEvent::Summoned { id: d },
|
||||
]
|
||||
);
|
||||
assert_eq!(g.name_of(d), "Damaged Golem");
|
||||
assert_eq!(g.board(0), vec![d, r]);
|
||||
assert_eq!(g.zone_of(h), Zone::Graveyard);
|
||||
assert_eq!(g.zone_of(golem), Zone::Graveyard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_03_simultaneous_summons_keep_board_order() {
|
||||
let mut g = new_game(GameOpts::default());
|
||||
let a = g.summon("Harvest Golem", 0);
|
||||
let b = g.summon("Harvest Golem", 0);
|
||||
let r = g.summon("Test Reactor", 0);
|
||||
g.deal_damage(a, 2);
|
||||
g.deal_damage(b, 2);
|
||||
g.clear_log();
|
||||
|
||||
g.cast("Whirlwind", 0, None);
|
||||
|
||||
let summoned = summoned_ids(&g.log());
|
||||
assert_eq!(summoned.len(), 2);
|
||||
let (da, db) = (summoned[0], summoned[1]);
|
||||
assert_eq!(
|
||||
g.log(),
|
||||
vec![
|
||||
whirlwind(),
|
||||
damaged(a, 1),
|
||||
damaged(b, 1),
|
||||
damaged(r, 1),
|
||||
GameEvent::Destroyed { id: a },
|
||||
GameEvent::Destroyed { id: b },
|
||||
GameEvent::DeathPhase { round: 1 },
|
||||
triggered(a, "deathrattle"),
|
||||
GameEvent::Summoned { id: da },
|
||||
triggered(b, "deathrattle"),
|
||||
GameEvent::Summoned { id: db },
|
||||
]
|
||||
);
|
||||
assert_eq!(g.name_of(da), "Damaged Golem");
|
||||
assert_eq!(g.name_of(db), "Damaged Golem");
|
||||
assert_eq!(g.board(0), vec![da, db, r]);
|
||||
assert_eq!(g.zone_of(a), Zone::Graveyard);
|
||||
assert_eq!(g.zone_of(b), Zone::Graveyard);
|
||||
}
|
||||
@@ -111,3 +111,72 @@ test("test_03_destroy_marks_pending", () => {
|
||||
expect(g.zoneOf(golem)).toBe("graveyard");
|
||||
expect(g.nameOf(d)).toBe("Damaged Golem");
|
||||
});
|
||||
|
||||
test("test_03_summon_position_after_simultaneous_deaths", () => {
|
||||
const g = newGame({ decks: [fillers(5), []] });
|
||||
const h = g.summon("Loot Hoarder", 0);
|
||||
const golem = g.summon("Harvest Golem", 0);
|
||||
const r = g.summon("Test Reactor", 0);
|
||||
g.dealDamage(golem, 2);
|
||||
g.clearLog();
|
||||
|
||||
g.cast("Whirlwind", 0);
|
||||
|
||||
const hand = g.hand(0);
|
||||
expect(hand).toHaveLength(1);
|
||||
const c = hand[0];
|
||||
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
|
||||
expect(summoned).toHaveLength(1);
|
||||
const d = summoned[0];
|
||||
expect(g.log()).toEqual([
|
||||
{ type: "SpellCast", card: "Whirlwind" },
|
||||
{ type: "Damaged", target: h, amount: 1 },
|
||||
{ type: "Damaged", target: golem, amount: 1 },
|
||||
{ type: "Damaged", target: r, amount: 1 },
|
||||
{ type: "Destroyed", id: h },
|
||||
{ type: "Destroyed", id: golem },
|
||||
{ type: "DeathPhase", round: 1 },
|
||||
{ type: "Triggered", source: h, trigger: "deathrattle" },
|
||||
{ type: "Drew", player: 0, id: c },
|
||||
{ type: "Triggered", source: golem, trigger: "deathrattle" },
|
||||
{ type: "Summoned", id: d },
|
||||
]);
|
||||
expect(g.nameOf(d)).toBe("Damaged Golem");
|
||||
expect(g.board(0)).toEqual([d, r]);
|
||||
expect(g.zoneOf(h)).toBe("graveyard");
|
||||
expect(g.zoneOf(golem)).toBe("graveyard");
|
||||
});
|
||||
|
||||
test("test_03_simultaneous_summons_keep_board_order", () => {
|
||||
const g = newGame();
|
||||
const a = g.summon("Harvest Golem", 0);
|
||||
const b = g.summon("Harvest Golem", 0);
|
||||
const r = g.summon("Test Reactor", 0);
|
||||
g.dealDamage(a, 2);
|
||||
g.dealDamage(b, 2);
|
||||
g.clearLog();
|
||||
|
||||
g.cast("Whirlwind", 0);
|
||||
|
||||
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
|
||||
expect(summoned).toHaveLength(2);
|
||||
const [da, db] = summoned;
|
||||
expect(g.log()).toEqual([
|
||||
{ type: "SpellCast", card: "Whirlwind" },
|
||||
{ type: "Damaged", target: a, amount: 1 },
|
||||
{ type: "Damaged", target: b, amount: 1 },
|
||||
{ type: "Damaged", target: r, amount: 1 },
|
||||
{ type: "Destroyed", id: a },
|
||||
{ type: "Destroyed", id: b },
|
||||
{ type: "DeathPhase", round: 1 },
|
||||
{ type: "Triggered", source: a, trigger: "deathrattle" },
|
||||
{ type: "Summoned", id: da },
|
||||
{ type: "Triggered", source: b, trigger: "deathrattle" },
|
||||
{ type: "Summoned", id: db },
|
||||
]);
|
||||
expect(g.nameOf(da)).toBe("Damaged Golem");
|
||||
expect(g.nameOf(db)).toBe("Damaged Golem");
|
||||
expect(g.board(0)).toEqual([da, db, r]);
|
||||
expect(g.zoneOf(a)).toBe("graveyard");
|
||||
expect(g.zoneOf(b)).toBe("graveyard");
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -69,8 +69,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>,
|
||||
}
|
||||
|
||||
@@ -205,7 +205,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 {
|
||||
@@ -224,6 +226,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> {
|
||||
|
||||
@@ -295,10 +295,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId };
|
||||
@@ -45,8 +45,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -161,7 +161,10 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
const actions = trigger.effect(new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
// Depth-first: each action, and every reaction it causes, finishes before the next.
|
||||
for (const action of actions) perform(world, action);
|
||||
@@ -169,6 +172,17 @@ function react(world: World, events: Event[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -225,10 +225,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
||||
}
|
||||
|
||||
/// Where a summon goes: to the right of a source still on the board; where a dead
|
||||
/// source stood (its Death Event's position); at the right end for a spell.
|
||||
/// source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
||||
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
||||
Some(index) => Some(index + 1),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -86,8 +86,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>,
|
||||
}
|
||||
|
||||
@@ -241,7 +241,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 owner = world.owner_of(source);
|
||||
@@ -250,6 +252,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> {
|
||||
|
||||
@@ -350,10 +350,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function targets(target: Target, ctx: Ctx): EntityId[] {
|
||||
|
||||
/**
|
||||
* Where a summon goes: to the right of a source still on the board; where a dead
|
||||
* source stood (its Death Event's position); at the right end for a spell.
|
||||
* source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
*/
|
||||
function summonPosition(ctx: Ctx): number | undefined {
|
||||
const index = ctx.source === undefined ? undefined : ctx.world.boardIndex(ctx.source);
|
||||
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId };
|
||||
@@ -55,8 +55,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -188,12 +188,26 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -275,10 +275,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
||||
}
|
||||
|
||||
/// Where a summon goes: to the right of a source still on the board; where a dead
|
||||
/// source stood (its Death Event's position); at the right end for a spell.
|
||||
/// source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
||||
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
||||
Some(index) => Some(index + 1),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -86,8 +86,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>,
|
||||
}
|
||||
|
||||
@@ -241,7 +241,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 owner = world.owner_of(source);
|
||||
@@ -250,6 +252,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> {
|
||||
|
||||
@@ -350,10 +350,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function targets(target: Target, ctx: Ctx): EntityId[] {
|
||||
|
||||
/**
|
||||
* Where a summon goes: to the right of a source still on the board; where a dead
|
||||
* source stood (its Death Event's position); at the right end for a spell.
|
||||
* source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
*/
|
||||
function summonPosition(ctx: Ctx): number | undefined {
|
||||
const index = ctx.source === undefined ? undefined : ctx.world.boardIndex(ctx.source);
|
||||
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId };
|
||||
@@ -55,8 +55,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -188,12 +188,26 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -275,10 +275,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
||||
}
|
||||
|
||||
/// Where a summon goes: to the right of a source still on the board; where a dead
|
||||
/// source stood (its Death Event's position); at the right end for a spell.
|
||||
/// source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
||||
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
||||
Some(index) => Some(index + 1),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -86,8 +86,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>,
|
||||
}
|
||||
|
||||
@@ -241,7 +241,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 owner = world.owner_of(source);
|
||||
@@ -250,6 +252,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> {
|
||||
|
||||
@@ -350,10 +350,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function targets(target: Target, ctx: Ctx): EntityId[] {
|
||||
|
||||
/**
|
||||
* Where a summon goes: to the right of a source still on the board; where a dead
|
||||
* source stood (its Death Event's position); at the right end for a spell.
|
||||
* source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
*/
|
||||
function summonPosition(ctx: Ctx): number | undefined {
|
||||
const index = ctx.source === undefined ? undefined : ctx.world.boardIndex(ctx.source);
|
||||
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId };
|
||||
@@ -55,8 +55,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -188,12 +188,26 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -275,10 +275,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
||||
}
|
||||
|
||||
/// Where a summon goes: to the right of a source still on the board; where a dead
|
||||
/// source stood (its Death Event's position); at the right end for a spell.
|
||||
/// source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
||||
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
||||
Some(index) => Some(index + 1),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -86,8 +86,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>,
|
||||
}
|
||||
|
||||
@@ -241,7 +241,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 owner = world.owner_of(source);
|
||||
@@ -250,6 +252,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> {
|
||||
|
||||
@@ -350,10 +350,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function targets(target: Target, ctx: Ctx): EntityId[] {
|
||||
|
||||
/**
|
||||
* Where a summon goes: to the right of a source still on the board; where a dead
|
||||
* source stood (its Death Event's position); at the right end for a spell.
|
||||
* source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
*/
|
||||
function summonPosition(ctx: Ctx): number | undefined {
|
||||
const index = ctx.source === undefined ? undefined : ctx.world.boardIndex(ctx.source);
|
||||
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId };
|
||||
@@ -55,8 +55,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -188,12 +188,26 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -275,10 +275,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
||||
}
|
||||
|
||||
/// Where a summon goes: to the right of a source still on the board; where a dead
|
||||
/// source stood (its Death Event's position); at the right end for a spell.
|
||||
/// source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
||||
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
||||
Some(index) => Some(index + 1),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -86,8 +86,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>,
|
||||
}
|
||||
|
||||
@@ -241,7 +241,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 owner = world.owner_of(source);
|
||||
@@ -250,6 +252,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> {
|
||||
|
||||
@@ -350,10 +350,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function targets(target: Target, ctx: Ctx): EntityId[] {
|
||||
|
||||
/**
|
||||
* Where a summon goes: to the right of a source still on the board; where a dead
|
||||
* source stood (its Death Event's position); at the right end for a spell.
|
||||
* source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
*/
|
||||
function summonPosition(ctx: Ctx): number | undefined {
|
||||
const index = ctx.source === undefined ? undefined : ctx.world.boardIndex(ctx.source);
|
||||
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId };
|
||||
@@ -55,8 +55,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -188,12 +188,26 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -275,10 +275,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -85,7 +85,7 @@ fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
||||
}
|
||||
|
||||
/// Where a summon goes: to the right of a source still on the board; where a dead
|
||||
/// source stood (its Death Event's position); at the right end for a spell.
|
||||
/// source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
||||
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
||||
Some(index) => Some(index + 1),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -110,8 +110,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>,
|
||||
}
|
||||
|
||||
@@ -270,7 +270,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 owner = world.owner_of(source);
|
||||
@@ -279,6 +281,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> {
|
||||
|
||||
@@ -383,10 +383,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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// ends. Then this step removes every marked minion at once.
|
||||
|
||||
import type { Event } from "./event";
|
||||
import type { World } from "./world";
|
||||
import type { EntityId, World } from "./world";
|
||||
|
||||
/**
|
||||
* Remove every mortally wounded or pending-destroy minion from play, simultaneously.
|
||||
@@ -13,9 +13,17 @@ import type { World } from "./world";
|
||||
*/
|
||||
export function deathCreationStep(world: World): Event[] {
|
||||
const dying = world.minionsInPlay().filter((id) => world.isDoomed(id));
|
||||
// Every position is read before anything moves: removal is simultaneous, and each
|
||||
// Death Event remembers where its minion stood when the step began.
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, position: world.boardIndex(id)! }));
|
||||
for (const { id, position } of deaths) world.kill(id, position);
|
||||
// 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).
|
||||
const deaths = dying.map((id) => ({ type: "Destroyed" as const, id, before: survivorToTheRight(world, id) }));
|
||||
for (const { id, before } of deaths) world.kill(id, before);
|
||||
return deaths;
|
||||
}
|
||||
|
||||
/** The nearest minion to the right of `id` on its board that is not leaving play in this step, if any. */
|
||||
function survivorToTheRight(world: World, id: EntityId): EntityId | undefined {
|
||||
const board = world.zone(world.ownerOf(id), "board");
|
||||
return board.slice(board.indexOf(id) + 1).find((other) => !world.isDoomed(other));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function targets(target: Target, ctx: Ctx): EntityId[] {
|
||||
|
||||
/**
|
||||
* Where a summon goes: to the right of a source still on the board; where a dead
|
||||
* source stood (its Death Event's position); at the right end for a spell.
|
||||
* source stood (left of the minion its Death Event remembers); at the right end for a spell.
|
||||
*/
|
||||
function summonPosition(ctx: Ctx): number | undefined {
|
||||
const index = ctx.source === undefined ? undefined : ctx.world.boardIndex(ctx.source);
|
||||
|
||||
@@ -14,9 +14,11 @@ export type Event =
|
||||
| { type: "SpellCast"; 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.
|
||||
*/
|
||||
| { type: "Destroyed"; id: EntityId; position: number }
|
||||
| { type: "Destroyed"; id: EntityId; before: EntityId | undefined }
|
||||
| { type: "DeathPhase"; round: number }
|
||||
/** A minion was played from hand (it has just entered the board). Its Battlecry answers this. */
|
||||
| { type: "Played"; id: EntityId }
|
||||
|
||||
@@ -75,8 +75,8 @@ export class Ctx {
|
||||
/** The chosen target, if any. */
|
||||
readonly target?: 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.
|
||||
*/
|
||||
readonly position?: number,
|
||||
) {}
|
||||
@@ -219,12 +219,26 @@ function react(world: World, events: Event[]): void {
|
||||
const times = activations(world, world.ownerOf(source), trigger.on);
|
||||
for (let i = 0; i < times; i++) {
|
||||
world.record({ type: "Triggered", source, on: trigger.on });
|
||||
const position = event.type === "Destroyed" && event.id === source ? event.position : world.boardIndex(source);
|
||||
const position =
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The board index, right now, immediately left of `before` on `owner`'s board; the
|
||||
* right end if `before` is undefined or no longer there. A Death Event remembers the
|
||||
* minion, not an index, so this still holds after other minions have left or arrived.
|
||||
*/
|
||||
function positionLeftOf(world: World, owner: PlayerId, before: EntityId | undefined): number {
|
||||
const board = world.zone(owner, "board");
|
||||
const index = before === undefined ? -1 : board.indexOf(before);
|
||||
return index < 0 ? board.length : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -308,10 +308,10 @@ export class World {
|
||||
}
|
||||
|
||||
/** Move a minion from play to its owner's graveyard, recording its Death Event. */
|
||||
kill(id: EntityId, position: number): void {
|
||||
kill(id: EntityId, before: EntityId | undefined): void {
|
||||
this.entity(id).pendingDestroy = false;
|
||||
this.moveTo(id, "graveyard");
|
||||
this.record({ type: "Destroyed", id, position });
|
||||
this.record({ type: "Destroyed", id, before });
|
||||
}
|
||||
|
||||
gainArmor(player: PlayerId, amount: number): void {
|
||||
|
||||
Reference in new issue
Block a user