// Test harness: the thin adapter the course tests call. It translates each // call into engine operations and holds no rules of its own. import { library } from "./content"; import { runSequence, summon, triggerLabel, World, type Event, type Zone } from "./engine"; export type { Zone }; export type Id = number; export type Player = 0 | 1; /** The events the tests read from `log()` (see course/harness.md). */ export type GameEvent = | { type: "Summoned"; id: Id } | { type: "Damaged"; target: Id; amount: number } | { type: "Triggered"; source: Id; trigger: string } | { type: "Destroyed"; id: Id } | { type: "DeathPhase"; round: number } | { type: "Drew"; player: Player; id: Id } | { type: "Fatigue"; player: Player } | { type: "ArmorGained"; player: Player; amount: number } | { type: "StatsChanged"; id: Id; attack: number; health: number } | { type: "SpellCast"; card: string }; /** The course's view of an engine event; `undefined` for engine events the course log doesn't show. */ function toGameEvent(event: Event): GameEvent | undefined { if (event.type === "Triggered") return { type: "Triggered", source: event.source, trigger: triggerLabel(event.on) }; if (event.type === "Destroyed") return { type: "Destroyed", id: event.id }; // A played minion's `Summoned` already shows it entering the board. if (event.type === "Played") return undefined; return { ...event }; } export interface Game { summon(card: string, player?: Player): Id; // to right end of board addToHand(card: string, player?: Player): Id; addToDeck(card: string, player?: Player): Id; play(handId: Id): void; // Module 1 Expansion Drop; required from Module 4 (Battlecry) zoneOf(id: Id): Zone; board(player: Player): Id[]; // left to right hand(player: Player): Id[]; orderOfPlay(id: Id): number; nameOf(id: Id): string; // Module 2 hero(player: Player): Id; dealDamage(target: Id, amount: number): void; // one Sequence cast(card: string, player?: Player, target?: Id): void; health(id: Id): number; attack(id: Id): number; armor(player: Player): number; log(): GameEvent[]; clearLog(): void; // Module 3 destroy(target: Id): void; // marks pending destroy, one Sequence } export function newGame(opts?: { decks?: [string[], string[]] }): Game { const world = new World(library()); opts?.decks?.forEach((deck, player) => { for (const card of deck) world.create(card, player as Player, "deck"); }); return { summon: (card, player = 0) => summon(world, card, player), addToHand: (card, player = 0) => world.create(card, player, "hand"), addToDeck: (card, player = 0) => world.create(card, player, "deck"), // Play a card from hand, as one Sequence: it enters the right end of the board, then its Battlecry resolves. play: (handId) => runSequence(world, { type: "play", id: handId }), zoneOf: (id) => world.zoneOf(id), board: (player) => [...world.zone(player, "board")], hand: (player) => [...world.zone(player, "hand")], orderOfPlay: (id) => { const order = world.orderOfPlay(id); if (order === undefined) throw new Error(`entity ${id} has not entered play`); return order; }, nameOf: (id) => world.cardName(id), hero: (player) => world.hero(player), dealDamage: (target, amount) => runSequence(world, { type: "damage", targets: [target], amount }), cast: (card, player = 0, target) => runSequence(world, { type: "cast", card, player, target }), health: (id) => world.health(id), attack: (id) => world.attack(id), armor: (player) => world.armor(player), log: () => world.log().flatMap((event) => toGameEvent(event) ?? []), clearLog: () => world.clearLog(), destroy: (target) => runSequence(world, { type: "destroy", targets: [target] }), }; }