feat: module 04 interceptors
Trigger multipliers as aura data on cards, snapshotted by an Aura Update (Other) after each Death Creation Step and on summon, and applied by one generic routine (activations) when each queued trigger starts. Baron Rivendare is content only (stacking "max"); Brann Bronzebeard is the content-only Expansion Drop. Adds play (now required) and Battlecry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a4eba1948
commit
bcdaca0cd2
40 files changed
+4421
-1
No files matched your search
@@ -0,0 +1,157 @@
|
||||
//! Test harness: the thin adapter the course tests call. It translates each
|
||||
//! call into engine operations and holds no rules of its own.
|
||||
|
||||
use crate::content;
|
||||
use crate::engine::{run_sequence, summon, Action, Event, World};
|
||||
|
||||
pub use crate::engine::Zone;
|
||||
|
||||
pub type Id = u32;
|
||||
pub type Player = u8;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct GameOpts {
|
||||
pub decks: Option<[Vec<String>; 2]>,
|
||||
}
|
||||
|
||||
/// The events the tests read from `log()` (see course/harness.md).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GameEvent {
|
||||
Summoned { id: Id },
|
||||
Damaged { target: Id, amount: i32 },
|
||||
Triggered { source: Id, trigger: String },
|
||||
Destroyed { id: Id },
|
||||
DeathPhase { round: u32 },
|
||||
Drew { player: Player, id: Id },
|
||||
Fatigue { player: Player },
|
||||
ArmorGained { player: Player, amount: i32 },
|
||||
StatsChanged { id: Id, attack: i32, health: i32 },
|
||||
SpellCast { card: String },
|
||||
}
|
||||
|
||||
impl GameEvent {
|
||||
/// The course's view of an engine event; `None` for engine events the course log
|
||||
/// doesn't show.
|
||||
fn from_engine(event: &Event) -> Option<GameEvent> {
|
||||
Some(match event.clone() {
|
||||
Event::Summoned { id } => GameEvent::Summoned { id },
|
||||
Event::Damaged { target, amount } => GameEvent::Damaged { target, amount },
|
||||
Event::Triggered { source, on } => GameEvent::Triggered {
|
||||
source,
|
||||
trigger: on.label().to_string(),
|
||||
},
|
||||
Event::Drew { player, id } => GameEvent::Drew { player, id },
|
||||
Event::Fatigue { player } => GameEvent::Fatigue { player },
|
||||
Event::ArmorGained { player, amount } => GameEvent::ArmorGained { player, amount },
|
||||
Event::StatsChanged { id, attack, health } => {
|
||||
GameEvent::StatsChanged { id, attack, health }
|
||||
}
|
||||
Event::SpellCast { card } => GameEvent::SpellCast { card },
|
||||
Event::Destroyed { id, .. } => GameEvent::Destroyed { id },
|
||||
Event::DeathPhase { round } => GameEvent::DeathPhase { round },
|
||||
// A played minion's `Summoned` already shows it entering the board.
|
||||
Event::Played { .. } => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
world: World,
|
||||
}
|
||||
|
||||
pub fn new_game(opts: GameOpts) -> Game {
|
||||
let mut world = World::new(content::library());
|
||||
if let Some(decks) = opts.decks {
|
||||
for (player, deck) in decks.iter().enumerate() {
|
||||
for card in deck {
|
||||
world.create(card, player as Player, Zone::Deck);
|
||||
}
|
||||
}
|
||||
}
|
||||
Game { world }
|
||||
}
|
||||
|
||||
impl Game {
|
||||
/// Put `card` at the right end of `player`'s board.
|
||||
pub fn summon(&mut self, card: &str, player: Player) -> Id {
|
||||
summon(&mut self.world, card, player, None)
|
||||
}
|
||||
pub fn add_to_hand(&mut self, card: &str, player: Player) -> Id {
|
||||
self.world.create(card, player, Zone::Hand)
|
||||
}
|
||||
pub fn add_to_deck(&mut self, card: &str, player: Player) -> Id {
|
||||
self.world.create(card, player, Zone::Deck)
|
||||
}
|
||||
/// Play a card from hand, as one Sequence: it enters the right end of the board, then
|
||||
/// its Battlecry resolves. (The Module 1 Expansion Drop; required from Module 4.)
|
||||
pub fn play(&mut self, hand_id: Id) {
|
||||
run_sequence(&mut self.world, Action::Play { id: hand_id });
|
||||
}
|
||||
pub fn zone_of(&self, id: Id) -> Zone {
|
||||
self.world.zone_of(id)
|
||||
}
|
||||
/// Board ids, left to right.
|
||||
pub fn board(&self, player: Player) -> Vec<Id> {
|
||||
self.world.zone(player, Zone::Board).to_vec()
|
||||
}
|
||||
pub fn hand(&self, player: Player) -> Vec<Id> {
|
||||
self.world.zone(player, Zone::Hand).to_vec()
|
||||
}
|
||||
pub fn order_of_play(&self, id: Id) -> u32 {
|
||||
self.world
|
||||
.order_of_play(id)
|
||||
.unwrap_or_else(|| panic!("entity {id} has not entered play"))
|
||||
}
|
||||
pub fn name_of(&self, id: Id) -> String {
|
||||
self.world.card_name(id).to_string()
|
||||
}
|
||||
|
||||
// ---- Module 2 ----
|
||||
|
||||
pub fn hero(&self, player: Player) -> Id {
|
||||
self.world.hero(player)
|
||||
}
|
||||
/// Deal damage to one character, as one Sequence.
|
||||
pub fn deal_damage(&mut self, target: Id, amount: i32) {
|
||||
let targets = vec![target];
|
||||
run_sequence(&mut self.world, Action::Damage { targets, amount });
|
||||
}
|
||||
pub fn cast(&mut self, card: &str, player: Player, target: Option<Id>) {
|
||||
let card = card.to_string();
|
||||
run_sequence(
|
||||
&mut self.world,
|
||||
Action::Cast {
|
||||
card,
|
||||
player,
|
||||
target,
|
||||
},
|
||||
);
|
||||
}
|
||||
pub fn health(&self, id: Id) -> i32 {
|
||||
self.world.health(id)
|
||||
}
|
||||
pub fn attack(&self, id: Id) -> i32 {
|
||||
self.world.attack(id)
|
||||
}
|
||||
pub fn armor(&self, player: Player) -> i32 {
|
||||
self.world.armor(player)
|
||||
}
|
||||
pub fn log(&self) -> Vec<GameEvent> {
|
||||
self.world
|
||||
.log()
|
||||
.iter()
|
||||
.filter_map(GameEvent::from_engine)
|
||||
.collect()
|
||||
}
|
||||
pub fn clear_log(&mut self) {
|
||||
self.world.clear_log();
|
||||
}
|
||||
|
||||
// ---- Module 3 ----
|
||||
|
||||
/// Mark `target` pending destroy, as one Sequence. It leaves play in the death step.
|
||||
pub fn destroy(&mut self, target: Id) {
|
||||
let targets = vec![target];
|
||||
run_sequence(&mut self.world, Action::Destroy { targets });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user