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 <[email protected]>
6.7 KiB
Harness contract
The harness is the thin adapter between the course tests and your engine. Tests only ever call harness functions. TypeScript signatures are canonical; Rust uses the same names in snake_case on pub struct Game / pub struct Baba / pub struct SpellGrid in src/harness.rs, with pub type Id = u32;, pub type Player = u8;, and events as pub enum GameEvent. Rust has no default arguments, so optional parameters (player, target) are explicit there.
Rust tests: use engine::harness::*;. TS tests: from "../src/harness".
Each module adds functions; start-module prints the line for the module you start.
Part I (Game): additions per module
- M1:
newGame(opts?: { decks?: [string[], string[]] }): Game;summon(card: string, player?: 0|1): Id(to right end of board);addToHand(card, player?),addToDeck(card, player?);play(handId: Id): void(Expansion Drop; required from M4);zoneOf(id): "deck"|"hand"|"board"|"graveyard";board(player): Id[](left to right);hand(player): Id[];orderOfPlay(id): number;nameOf(id): string. - M2:
hero(player): Id;dealDamage(target: Id, amount: number): void(one Sequence);cast(card: string, player?, target?: Id): void;health(id),attack(id),armor(player);log(): GameEvent[];clearLog(). - M3:
destroy(target: Id): void(marks pending-destroy, one Sequence). - M4: no new functions;
play(handId)(the M1 Expansion Drop) is now required: it runs one Sequence in which the minion enters the right end of its owner's board (Summoned{id}), then its Battlecry resolves (Triggered{source, trigger: "battlecry"}). The M4 tests also useBaron Rivendare,Novice EngineerandTwisting Nether(the M3 Expansion Drop card). - M5:
registerCards(defs: CardDef[]): void;assemble(base: string, donor: string, half: "top"|"bottom"): string(registers and returns new card name"<base> (Assembled)"; throwsAssembleErrorif text unchanged).
Side by side (Part I)
| Module | TypeScript | Rust (impl Game unless noted) |
|---|---|---|
| M1 | newGame(opts?) |
new_game(opts: GameOpts) -> Game (free function) |
| M1 | summon(card, player?): Id |
summon(&mut self, card: &str, player: Player) -> Id |
| M1 | addToHand(card, player?) |
add_to_hand(&mut self, card: &str, player: Player) -> Id |
| M1 | addToDeck(card, player?) |
add_to_deck(&mut self, card: &str, player: Player) -> Id |
| M1 | play(handId) |
play(&mut self, hand_id: Id) |
| M1 | zoneOf(id) |
zone_of(&self, id: Id) -> Zone |
| M1 | board(player) / hand(player) |
board(&self, player) -> Vec<Id> / hand(...) |
| M1 | orderOfPlay(id) |
order_of_play(&self, id: Id) -> u32 |
| M1 | nameOf(id) |
name_of(&self, id: Id) -> String |
| M2 | hero(player) |
hero(&self, player) -> Id |
| M2 | dealDamage(target, amount) |
deal_damage(&mut self, target: Id, amount: i32) |
| M2 | cast(card, player?, target?) |
cast(&mut self, card: &str, player: Player, target: Option<Id>) |
| M2 | health(id), attack(id), armor(player) |
health, attack, armor |
| M2 | log(), clearLog() |
log(&self) -> Vec<GameEvent>, clear_log(&mut self) |
| M3 | destroy(target) |
destroy(&mut self, target: Id) |
| M4 | play(handId) now required (one Sequence; Battlecry) |
play(&mut self, hand_id: Id) |
| M5 | registerCards(defs) |
register_cards(&mut self, defs: Vec<CardDef>) |
| M5 | assemble(base, donor, half) (throws AssembleError) |
assemble(...) -> Result<String, AssembleError> |
GameEvent
Tagged by type: Summoned{id}, Damaged{target, amount}, Triggered{source, trigger} (trigger: "deathrattle"|"battlecry"|"damaged"|...), Destroyed{id} (removed in a Death Creation Step), DeathPhase{round}, Drew{player, id}, Fatigue{player}, ArmorGained{player, amount}, StatsChanged{id, attack, health}, SpellCast{card}.
Drawing from an empty deck logs Fatigue{player} and does nothing else; the engine must not panic.
Rust shape (settled in Module 2), defined in src/harness.rs with #[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 },
}
In TypeScript each event is a plain object with a type field, for example { type: "Damaged", target, amount }. Health, Attack, Armor and damage amounts are i32 in Rust (Health can go to 0 or below). Each player has a hero with 30 Health from the start of the game; hero(player) returns its id. Heroes are in play but are not in board(player), which lists minions only. cast takes the card name; a spell is not an entity.
Part II (Baba): additions per module
- M6:
newBaba(level: LevelDef): Baba;place(kind: string, x, y): Id(kind"baba"or"text:BABA");rules(): string[](sorted, normalised"BABA IS YOU", includes"TEXT IS PUSH"). - M7:
hasProperty(id, prop: string): boolean;activeRules(): string[](after NOT/identity post-pass). - M8:
move(dir: "up"|"down"|"left"|"right"): void(movement only, then reparse);posOf(id): [number, number] | null(null once destroyed);kindsAt(x, y): string[](sorted). - M9:
step(dir): void(full simplified turn);won(): boolean;lost(): boolean(no YOU objects remain).
Grid coordinates: x grows right, y grows down; vertical sentences read top-to-bottom.
Side by side (Part II)
| Module | TypeScript | Rust (impl Baba unless noted) |
|---|---|---|
| M6 | newBaba(level) |
new_baba(level: LevelDef) -> Baba (free function) |
| M6 | place(kind, x, y): Id |
place(&mut self, kind: &str, x: i32, y: i32) -> Id |
| M6 | rules() |
rules(&self) -> Vec<String> |
| M7 | hasProperty(id, prop) |
has_property(&self, id: Id, prop: &str) -> bool |
| M7 | activeRules() |
active_rules(&self) -> Vec<String> |
| M8 | move(dir) |
move_dir(&mut self, dir: Dir) (move is a Rust keyword) |
| M8 | posOf(id) |
pos_of(&self, id: Id) -> Option<(i32, i32)> |
| M8 | kindsAt(x, y) |
kinds_at(&self, x: i32, y: i32) -> Vec<String> |
| M9 | step(dir) |
step(&mut self, dir: Dir) |
| M9 | won(), lost() |
won(&self) -> bool, lost(&self) -> bool |
Part III (SpellGrid)
- M10:
newSpellGrid(level: LevelDef, cards: CardDef[]): SpellGridexposing all of Part II pluscast,health,log,hero,won. Minions are grid objects whose noun is their card's"noun"field.
Rust: new_spell_grid(level: LevelDef, cards: Vec<CardDef>) -> SpellGrid.