# 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 use `Baron Rivendare`, `Novice Engineer` and `Twisting 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 `" (Assembled)"`; throws `AssembleError` if 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` / `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)` | | M2 | `health(id)`, `attack(id)`, `armor(player)` | `health`, `attack`, `armor` | | M2 | `log()`, `clearLog()` | `log(&self) -> Vec`, `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)` | | M5 | `assemble(base, donor, half)` (throws `AssembleError`) | `assemble(...) -> Result` | ### `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)]`: ```rust 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` | | M7 | `hasProperty(id, prop)` | `has_property(&self, id: Id, prop: &str) -> bool` | | M7 | `activeRules()` | `active_rules(&self) -> Vec` | | 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` | | 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[]): SpellGrid` exposing all of Part II plus `cast`, `health`, `log`, `hero`, `won`. Minions are grid objects whose noun is their card's `"noun"` field. Rust: `new_spell_grid(level: LevelDef, cards: Vec) -> SpellGrid`.