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 <noreply@anthropic.com>
91 lines
3.7 KiB
TypeScript
91 lines
3.7 KiB
TypeScript
// 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] }),
|
|
};
|
|
}
|