feat: module 02 action queue
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78dc09b303
commit
9c50591d3b
36 files changed
+3616
No files matched your search
@@ -0,0 +1,141 @@
|
||||
// Module 2: The action queue, depth-first. Each test matches a heading in scenarios.md.
|
||||
use engine::harness::*;
|
||||
|
||||
fn fillers(n: usize) -> Vec<String> {
|
||||
vec!["Test Filler".to_string(); n]
|
||||
}
|
||||
|
||||
/// Sources of the `Triggered` events, in log order.
|
||||
fn triggered_sources(log: &[GameEvent]) -> Vec<Id> {
|
||||
log.iter()
|
||||
.filter_map(|e| match e {
|
||||
GameEvent::Triggered { source, .. } => Some(*source),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_02_aoe_damage_then_triggers() {
|
||||
let mut g = new_game(GameOpts {
|
||||
decks: Some([fillers(5), fillers(5)]),
|
||||
});
|
||||
let s = g.summon("Armorsmith", 0);
|
||||
let a = g.summon("Acolyte of Pain", 0);
|
||||
g.clear_log();
|
||||
|
||||
g.cast("Whirlwind", 0, None);
|
||||
let log = g.log();
|
||||
|
||||
let damaged: Vec<GameEvent> = log
|
||||
.iter()
|
||||
.filter(|e| matches!(e, GameEvent::Damaged { .. }))
|
||||
.cloned()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
damaged,
|
||||
vec![
|
||||
GameEvent::Damaged {
|
||||
target: s,
|
||||
amount: 1
|
||||
},
|
||||
GameEvent::Damaged {
|
||||
target: a,
|
||||
amount: 1
|
||||
},
|
||||
]
|
||||
);
|
||||
let last_damaged = log
|
||||
.iter()
|
||||
.rposition(|e| matches!(e, GameEvent::Damaged { .. }))
|
||||
.unwrap();
|
||||
let first_triggered = log
|
||||
.iter()
|
||||
.position(|e| matches!(e, GameEvent::Triggered { .. }))
|
||||
.unwrap();
|
||||
assert!(last_damaged < first_triggered);
|
||||
|
||||
let sources = triggered_sources(&log);
|
||||
assert_eq!(sources.iter().filter(|&&id| id == s).count(), 2);
|
||||
assert_eq!(sources.iter().filter(|&&id| id == a).count(), 1);
|
||||
assert_eq!(sources.len(), 3);
|
||||
assert_eq!(g.armor(0), 2);
|
||||
assert_eq!(g.hand(0).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_02_mortally_wounded_still_triggers() {
|
||||
let mut g = new_game(GameOpts {
|
||||
decks: Some([fillers(5), vec![]]),
|
||||
});
|
||||
let a = g.summon("Acolyte of Pain", 0);
|
||||
g.deal_damage(a, 3);
|
||||
let n = g.hand(0).len();
|
||||
g.clear_log();
|
||||
|
||||
g.cast("Whirlwind", 0, None);
|
||||
let log = g.log();
|
||||
|
||||
assert!(log.contains(&GameEvent::Triggered {
|
||||
source: a,
|
||||
trigger: "damaged".to_string(),
|
||||
}));
|
||||
assert!(log
|
||||
.iter()
|
||||
.any(|e| matches!(e, GameEvent::Drew { player: 0, .. })));
|
||||
assert_eq!(g.hand(0).len(), n + 1);
|
||||
assert_eq!(g.health(a), 0);
|
||||
assert_eq!(g.zone_of(a), Zone::Board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_02_depth_first_nesting() {
|
||||
let mut g = new_game(GameOpts::default());
|
||||
let e = g.summon("Test Echo", 0);
|
||||
let s = g.summon("Armorsmith", 0);
|
||||
let r = g.summon("Test Reactor", 0);
|
||||
g.clear_log();
|
||||
|
||||
g.deal_damage(s, 1);
|
||||
let log = g.log();
|
||||
|
||||
assert_eq!(triggered_sources(&log), vec![e, r, s]);
|
||||
let reactor_grew = log
|
||||
.iter()
|
||||
.position(|ev| matches!(ev, GameEvent::StatsChanged { id, .. } if *id == r))
|
||||
.unwrap();
|
||||
let armor_gained = log
|
||||
.iter()
|
||||
.position(|ev| matches!(ev, GameEvent::ArmorGained { .. }))
|
||||
.unwrap();
|
||||
assert!(reactor_grew < armor_gained);
|
||||
assert_eq!(g.attack(r), 2);
|
||||
assert_eq!(g.armor(0), 1);
|
||||
assert_eq!(g.health(g.hero(1)), 29);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_02_empty_deck_draw_is_fatigue() {
|
||||
let mut g = new_game(GameOpts::default());
|
||||
let a = g.summon("Acolyte of Pain", 0);
|
||||
g.clear_log();
|
||||
|
||||
g.deal_damage(a, 1);
|
||||
|
||||
assert_eq!(
|
||||
g.log(),
|
||||
vec![
|
||||
GameEvent::Damaged {
|
||||
target: a,
|
||||
amount: 1
|
||||
},
|
||||
GameEvent::Triggered {
|
||||
source: a,
|
||||
trigger: "damaged".to_string(),
|
||||
},
|
||||
GameEvent::Fatigue { player: 0 },
|
||||
]
|
||||
);
|
||||
assert_eq!(g.hand(0), Vec::<Id>::new());
|
||||
assert_eq!(g.health(g.hero(0)), 30);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Module 2: The action queue, depth-first. Each test matches a heading in scenarios.md.
|
||||
import { expect, test } from "vitest";
|
||||
import { newGame, type GameEvent, type Id } from "../src/harness";
|
||||
|
||||
const fillers = (n: number): string[] => Array(n).fill("Test Filler");
|
||||
|
||||
/** Sources of the `Triggered` events, in log order. */
|
||||
const triggeredSources = (log: GameEvent[]): Id[] =>
|
||||
log.flatMap((e) => (e.type === "Triggered" ? [e.source] : []));
|
||||
|
||||
test("test_02_aoe_damage_then_triggers", () => {
|
||||
const g = newGame({ decks: [fillers(5), fillers(5)] });
|
||||
const s = g.summon("Armorsmith", 0);
|
||||
const a = g.summon("Acolyte of Pain", 0);
|
||||
g.clearLog();
|
||||
|
||||
g.cast("Whirlwind", 0);
|
||||
const log = g.log();
|
||||
|
||||
expect(log.filter((e) => e.type === "Damaged")).toEqual([
|
||||
{ type: "Damaged", target: s, amount: 1 },
|
||||
{ type: "Damaged", target: a, amount: 1 },
|
||||
]);
|
||||
const types = log.map((e) => e.type);
|
||||
const lastDamaged = types.lastIndexOf("Damaged");
|
||||
const firstTriggered = types.indexOf("Triggered");
|
||||
expect(firstTriggered).toBeGreaterThanOrEqual(0);
|
||||
expect(lastDamaged).toBeLessThan(firstTriggered);
|
||||
|
||||
const sources = triggeredSources(log);
|
||||
expect(sources.filter((id) => id === s)).toHaveLength(2);
|
||||
expect(sources.filter((id) => id === a)).toHaveLength(1);
|
||||
expect(sources).toHaveLength(3);
|
||||
expect(g.armor(0)).toBe(2);
|
||||
expect(g.hand(0)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("test_02_mortally_wounded_still_triggers", () => {
|
||||
const g = newGame({ decks: [fillers(5), []] });
|
||||
const a = g.summon("Acolyte of Pain", 0);
|
||||
g.dealDamage(a, 3);
|
||||
const n = g.hand(0).length;
|
||||
g.clearLog();
|
||||
|
||||
g.cast("Whirlwind", 0);
|
||||
const log = g.log();
|
||||
|
||||
expect(log).toContainEqual({ type: "Triggered", source: a, trigger: "damaged" });
|
||||
expect(log.some((e) => e.type === "Drew" && e.player === 0)).toBe(true);
|
||||
expect(g.hand(0)).toHaveLength(n + 1);
|
||||
expect(g.health(a)).toBe(0);
|
||||
expect(g.zoneOf(a)).toBe("board");
|
||||
});
|
||||
|
||||
test("test_02_depth_first_nesting", () => {
|
||||
const g = newGame();
|
||||
const e = g.summon("Test Echo", 0);
|
||||
const s = g.summon("Armorsmith", 0);
|
||||
const r = g.summon("Test Reactor", 0);
|
||||
g.clearLog();
|
||||
|
||||
g.dealDamage(s, 1);
|
||||
const log = g.log();
|
||||
|
||||
expect(triggeredSources(log)).toEqual([e, r, s]);
|
||||
const reactorGrew = log.findIndex((ev) => ev.type === "StatsChanged" && ev.id === r);
|
||||
const armorGained = log.findIndex((ev) => ev.type === "ArmorGained");
|
||||
expect(reactorGrew).toBeGreaterThanOrEqual(0);
|
||||
expect(reactorGrew).toBeLessThan(armorGained);
|
||||
expect(g.attack(r)).toBe(2);
|
||||
expect(g.armor(0)).toBe(1);
|
||||
expect(g.health(g.hero(1))).toBe(29);
|
||||
});
|
||||
|
||||
test("test_02_empty_deck_draw_is_fatigue", () => {
|
||||
const g = newGame();
|
||||
const a = g.summon("Acolyte of Pain", 0);
|
||||
g.clearLog();
|
||||
|
||||
g.dealDamage(a, 1);
|
||||
|
||||
expect(g.log()).toEqual([
|
||||
{ type: "Damaged", target: a, amount: 1 },
|
||||
{ type: "Triggered", source: a, trigger: "damaged" },
|
||||
{ type: "Fatigue", player: 0 },
|
||||
]);
|
||||
expect(g.hand(0)).toEqual([]);
|
||||
expect(g.health(g.hero(0))).toBe(30);
|
||||
});
|
||||
Reference in new issue
Block a user