feat: module 0 rewrite trap
Naive engine in Rust and TypeScript (immediate damage, inline triggers, recursive Deathrattles, cards defined in the engine) with test_00_naive_runs and the expected-fail test_00_real_rules_whirlwind_order, plus the Module 0 lesson. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
00662639ba
commit
ffa99785c3
13 files changed
+2121
No files matched your search
@@ -0,0 +1,93 @@
|
||||
// The naive engine for Module 0, The Rewrite Trap.
|
||||
//
|
||||
// It is written the "obvious" way on purpose, and it is wrong in ways worth
|
||||
// studying. Read course/00-rewrite-trap/lesson.md before you change it.
|
||||
// You throw this engine away at the end of Module 0.
|
||||
|
||||
/** What a card does, written as a plain function the engine calls directly. */
|
||||
export type Effect = (game: Game, self: Minion) => void;
|
||||
|
||||
/** A minion is shared by reference: the board and anyone holding it see the same object. */
|
||||
export interface Minion {
|
||||
name: string;
|
||||
attack: number;
|
||||
health: number;
|
||||
/** Called the moment this minion takes damage. */
|
||||
onDamaged?: Effect;
|
||||
/** Called the moment this minion dies. */
|
||||
deathrattle?: Effect;
|
||||
}
|
||||
|
||||
export class Game {
|
||||
/** The minions on the board, left to right. There is only one player. */
|
||||
board: Minion[] = [];
|
||||
cardsDrawn = 0;
|
||||
/** What happened, in order, as plain text. */
|
||||
log: string[] = [];
|
||||
|
||||
/** Puts a minion on the right end of the board and returns it. */
|
||||
summon(card: string): Minion {
|
||||
const minion = makeMinion(card);
|
||||
this.board.push(minion);
|
||||
return minion;
|
||||
}
|
||||
|
||||
cast(card: string): void {
|
||||
switch (card) {
|
||||
// Deal 1 damage to ALL minions.
|
||||
case "Whirlwind":
|
||||
for (const minion of [...this.board]) {
|
||||
this.dealDamage(minion, 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown spell: ${card}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Damage happens now. The damage trigger fires now. Death happens now. */
|
||||
dealDamage(target: Minion, amount: number): void {
|
||||
target.health -= amount;
|
||||
this.log.push(`damaged ${target.name} ${amount}`);
|
||||
if (target.onDamaged) {
|
||||
this.log.push(`triggered ${target.name}`);
|
||||
target.onDamaged(this, target);
|
||||
}
|
||||
if (target.health <= 0) {
|
||||
this.kill(target);
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes the minion and runs its Deathrattle straight away. */
|
||||
private kill(target: Minion): void {
|
||||
this.board = this.board.filter((minion) => minion !== target);
|
||||
this.log.push(`died ${target.name}`);
|
||||
if (target.deathrattle) {
|
||||
this.log.push(`deathrattle ${target.name}`);
|
||||
target.deathrattle(this, target);
|
||||
}
|
||||
}
|
||||
|
||||
draw(): void {
|
||||
this.cardsDrawn += 1;
|
||||
this.log.push("drew a card");
|
||||
}
|
||||
|
||||
boardNames(): string[] {
|
||||
return this.board.map((minion) => minion.name);
|
||||
}
|
||||
}
|
||||
|
||||
/** Every minion the game knows. A new card means a new case here. */
|
||||
function makeMinion(card: string): Minion {
|
||||
switch (card) {
|
||||
// Whenever this minion takes damage, draw a card.
|
||||
case "Acolyte of Pain":
|
||||
return { name: card, attack: 1, health: 4, onDamaged: (game) => game.draw() };
|
||||
// Deathrattle: Draw a card.
|
||||
case "Loot Hoarder":
|
||||
return { name: card, attack: 2, health: 1, deathrattle: (game) => game.draw() };
|
||||
default:
|
||||
throw new Error(`unknown minion: ${card}`);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user