Naive engine in Rust and TypeScript (immediate damage, inline triggers, recursive Deathrattles, cards defined in the engine) with test_00_naive_runs and the expected-fail test_00_real_rules_whirlwind_order, plus the Module 0 lesson. Co-Authored-By: Claude Opus 5.5 <[email protected]>
117 lines
3.7 KiB
Rust
117 lines
3.7 KiB
Rust
//! The naive engine for Module 0, The Rewrite Trap.
|
|
//!
|
|
//! It is written the "obvious" way on purpose, and it is wrong in ways worth
|
|
//! studying. Read course/00-rewrite-trap/lesson.md before you change it.
|
|
//! You throw this engine away at the end of Module 0.
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
/// A minion is shared by reference: the board and anyone holding it see the same one.
|
|
pub type MinionRef = Rc<RefCell<Minion>>;
|
|
|
|
/// What a card does, written as a plain function the engine calls directly.
|
|
pub type Effect = fn(&mut Game, &MinionRef);
|
|
|
|
pub struct Minion {
|
|
pub name: String,
|
|
pub attack: i32,
|
|
pub health: i32,
|
|
/// Called the moment this minion takes damage.
|
|
pub on_damaged: Option<Effect>,
|
|
/// Called the moment this minion dies.
|
|
pub deathrattle: Option<Effect>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct Game {
|
|
/// The minions on the board, left to right. There is only one player.
|
|
pub board: Vec<MinionRef>,
|
|
pub cards_drawn: u32,
|
|
/// What happened, in order, as plain text.
|
|
pub log: Vec<String>,
|
|
}
|
|
|
|
impl Game {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Puts a minion on the right end of the board and returns it.
|
|
pub fn summon(&mut self, card: &str) -> MinionRef {
|
|
let minion = Rc::new(RefCell::new(make_minion(card)));
|
|
self.board.push(Rc::clone(&minion));
|
|
minion
|
|
}
|
|
|
|
pub fn cast(&mut self, card: &str) {
|
|
match card {
|
|
// Deal 1 damage to ALL minions.
|
|
"Whirlwind" => {
|
|
for minion in self.board.clone() {
|
|
self.deal_damage(&minion, 1);
|
|
}
|
|
}
|
|
other => panic!("unknown spell: {other}"),
|
|
}
|
|
}
|
|
|
|
/// Damage happens now. The damage trigger fires now. Death happens now.
|
|
pub fn deal_damage(&mut self, target: &MinionRef, amount: i32) {
|
|
let (name, on_damaged) = {
|
|
let mut minion = target.borrow_mut();
|
|
minion.health -= amount;
|
|
(minion.name.clone(), minion.on_damaged)
|
|
};
|
|
self.log.push(format!("damaged {name} {amount}"));
|
|
if let Some(effect) = on_damaged {
|
|
self.log.push(format!("triggered {name}"));
|
|
effect(self, target);
|
|
}
|
|
if target.borrow().health <= 0 {
|
|
self.kill(target);
|
|
}
|
|
}
|
|
|
|
/// Removes the minion and runs its Deathrattle straight away.
|
|
fn kill(&mut self, target: &MinionRef) {
|
|
self.board.retain(|minion| !Rc::ptr_eq(minion, target));
|
|
let (name, deathrattle) = {
|
|
let minion = target.borrow();
|
|
(minion.name.clone(), minion.deathrattle)
|
|
};
|
|
self.log.push(format!("died {name}"));
|
|
if let Some(effect) = deathrattle {
|
|
self.log.push(format!("deathrattle {name}"));
|
|
effect(self, target);
|
|
}
|
|
}
|
|
|
|
pub fn draw(&mut self) {
|
|
self.cards_drawn += 1;
|
|
self.log.push("drew a card".to_string());
|
|
}
|
|
|
|
pub fn board_names(&self) -> Vec<String> {
|
|
self.board.iter().map(|minion| minion.borrow().name.clone()).collect()
|
|
}
|
|
}
|
|
|
|
/// Every minion the game knows. A new card means a new branch here.
|
|
fn make_minion(card: &str) -> Minion {
|
|
let stats = |attack, health| Minion {
|
|
name: card.to_string(),
|
|
attack,
|
|
health,
|
|
on_damaged: None,
|
|
deathrattle: None,
|
|
};
|
|
match card {
|
|
// Whenever this minion takes damage, draw a card.
|
|
"Acolyte of Pain" => Minion { on_damaged: Some(|game, _| game.draw()), ..stats(1, 4) },
|
|
// Deathrattle: Draw a card.
|
|
"Loot Hoarder" => Minion { deathrattle: Some(|game, _| game.draw()), ..stats(2, 1) },
|
|
other => panic!("unknown minion: {other}"),
|
|
}
|
|
}
|