5.3 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);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). - 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) |
| 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.
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.