Implement casting layer: attack stack, counteraction chain, first effects

The attack/counteraction stack: casting an attack (or punching) opens a
stack; the defender counteracts or passes, the attacker may respond
(ANTI-ANTI nullifies a counter), and resolution runs the damage
pipeline in play order. First effect wave, encoded from verified card
text: Fireball (flat 5 + destroys carried magic stones if damage gets
through), Lightning Blast (number damage + stun unless fully stopped),
Powerthrust (2 + optional number), Waterbolt (caster-chosen
damage/knockback split with push-away movement), Blunt (halve, round
result up), Absorb (-3), Full Shield (spell-only stop), Reflection
(half to both), Full Reflection (redirect), Absorb Spell (nullify and
steal the attack card), Create/Destroy Wall (dynamic edge overrides
layered over the board; collapse deals 4 to adjacent squares), Speed
(extra turn), and TRAP! on draw (lose next turn, redraw). Lost turns
skip on advance; unimplemented cards refuse to cast with a clear
error. 35 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 19:47:06 -04:00
co-authored by Claude Fable 5
parent a8884592a4
commit 7c607ad5c7
4 changed files with 974 additions and 70 deletions
+14 -15
View File
@@ -115,17 +115,17 @@ describe("turns and combat", () => {
const attacker = activePlayer(state);
const victim = state.players.find((p) => p.id !== attacker.id)!;
victim.position = { ...attacker.position };
const afterPunch = applyCommand(state, attacker.id, { type: "punch", targetId: victim.id });
expect(afterPunch.ok).toBe(true);
if (afterPunch.ok) {
const v = afterPunch.state.players.find((p) => p.id === victim.id)!;
expect(v.life).toBe(STARTING_LIFE - 1);
const secondPunch = applyCommand(afterPunch.state, attacker.id, {
type: "punch",
targetId: victim.id,
});
expect(secondPunch.ok).toBe(false);
}
state = must(state, attacker.id, { type: "punch", targetId: victim.id });
// Punches enter the counteraction stack (BLUNT/ABSORB work on physical
// damage); the defender passes and the punch resolves.
state = must(state, victim.id, { type: "pass" });
const v = state.players.find((p) => p.id === victim.id)!;
expect(v.life).toBe(STARTING_LIFE - 1);
const secondPunch = applyCommand(state, attacker.id, {
type: "punch",
targetId: victim.id,
});
expect(secondPunch.ok).toBe(false);
});
it("cannot punch yourself", () => {
@@ -145,11 +145,10 @@ describe("turns and combat", () => {
const victim = state.players.find((p) => p.id !== attacker.id)!;
victim.position = { ...attacker.position };
victim.life = 1; // test surgery: one punch kills
const result = applyCommand(state, attacker.id, { type: "punch", targetId: victim.id });
expect(result.ok).toBe(true);
if (!result.ok) return;
state = must(state, attacker.id, { type: "punch", targetId: victim.id });
state = must(state, victim.id, { type: "pass" });
const s = result.state;
const s = state;
expect(s.players.find((p) => p.id === victim.id)!.alive).toBe(false);
const a = s.players.find((p) => p.id === attacker.id)!;
expect(a.hand.length).toBe(14);