Rev 18: the democratic monster's claw outlives the first wizard
Its one-attack-per-round refreshed inside beginTurnFor when the turn index matched the roll-off winner's seat — and only the living begin turns, so once that wizard fell the claw never refreshed again: one attack, then a lifetime of harmless bumping. The refresh now lives at the true round boundary in the turn-advance loop, which passes dead seats without skipping the count. Earlier ledgers keep their gummed monster and replay so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d89d995daf
commit
3b0aa01867
@@ -5505,8 +5505,9 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi
|
|||||||
c.scorchedThisTurn = [];
|
c.scorchedThisTurn = [];
|
||||||
if (c.kind === "democratic-monster") {
|
if (c.kind === "democratic-monster") {
|
||||||
c.movementUsed = 0;
|
c.movementUsed = 0;
|
||||||
// its single attack refreshes per ROUND: on the first player's turn
|
// Rev <18 refreshed the claw here — which never fires once the
|
||||||
if (index === state.turn.firstIndex) c.attackUsed = false;
|
// first-rolled wizard is dead, since only the living begin turns.
|
||||||
|
if ((state.config.deckRev ?? 1) < 18 && index === state.turn.firstIndex) c.attackUsed = false;
|
||||||
} else if (c.controllerId === player.id) {
|
} else if (c.controllerId === player.id) {
|
||||||
c.movementUsed = 0;
|
c.movementUsed = 0;
|
||||||
c.wallPassUsed = 0;
|
c.wallPassUsed = 0;
|
||||||
@@ -5709,7 +5710,17 @@ function doEndTurn(prev: GameState, draw: number): CommandResult {
|
|||||||
let next = state.turn.activeIndex;
|
let next = state.turn.activeIndex;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
next = (next + 1) % n;
|
next = (next + 1) % n;
|
||||||
if (next === state.turn.firstIndex) state.turn.round++;
|
if (next === state.turn.firstIndex) {
|
||||||
|
state.turn.round++;
|
||||||
|
// The democratic monster's one claw per round refreshes at the TRUE
|
||||||
|
// round boundary — even when the first-rolled wizard is dead or
|
||||||
|
// skipped and never begins a turn (rules rev 18).
|
||||||
|
if ((state.config.deckRev ?? 1) >= 18) {
|
||||||
|
for (const c of state.creatures) {
|
||||||
|
if (c.kind === "democratic-monster") c.attackUsed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const candidate = state.players[next]!;
|
const candidate = state.players[next]!;
|
||||||
if (!candidate.alive) continue;
|
if (!candidate.alive) continue;
|
||||||
if (candidate.lostTurns > 0) {
|
if (candidate.lostTurns > 0) {
|
||||||
|
|||||||
@@ -589,3 +589,30 @@ describe("the wave carries monsters (rules rev 17)", () => {
|
|||||||
throw new Error("setup: no horizontal wall with a floor below");
|
throw new Error("setup: no horizontal wall with a floor below");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the democratic monster's claw survives the first wizard's death (rules rev 18)", () => {
|
||||||
|
it("refreshes each round even with the roll-off winner dead", () => {
|
||||||
|
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"], deckRev: 18 });
|
||||||
|
state = toRound2(state);
|
||||||
|
const firstId = state.players[state.turn.firstIndex]!.id;
|
||||||
|
const dm = {
|
||||||
|
id: "dm1", kind: "democratic-monster" as const, controllerId: state.players[0]!.id,
|
||||||
|
position: { x: 0, y: 0 }, damage: 0, maxDamage: 5, movesPerTurn: 3,
|
||||||
|
movementUsed: 0, attackUsed: true, justCreated: false,
|
||||||
|
wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [] as string[],
|
||||||
|
};
|
||||||
|
state.creatures.push(dm);
|
||||||
|
// The roll-off winner falls.
|
||||||
|
const first = state.players.find((p) => p.id === firstId)!;
|
||||||
|
first.alive = false;
|
||||||
|
first.finalHand = [...first.hand];
|
||||||
|
// Walk a full round of the survivors: the claw must refresh.
|
||||||
|
for (let i = 0; i < 4 && state.creatures[0]!.attackUsed; i++) {
|
||||||
|
const active = activePlayer(state);
|
||||||
|
const r = applyCommand(state, active.id, { type: "endTurn", draw: 0 });
|
||||||
|
if (!r.ok) throw new Error(r.error);
|
||||||
|
state = r.state;
|
||||||
|
}
|
||||||
|
expect(state.creatures[0]!.attackUsed).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export interface Room {
|
|||||||
const rooms = new Map<string, Room>();
|
const rooms = new Map<string, Room>();
|
||||||
|
|
||||||
/** Rules revision new games are dealt under (stored games keep their own). */
|
/** Rules revision new games are dealt under (stored games keep their own). */
|
||||||
const RULES_REV = 17;
|
const RULES_REV = 18;
|
||||||
|
|
||||||
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class LocalGame {
|
|||||||
seed,
|
seed,
|
||||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||||
...(colors ? { colors } : {}),
|
...(colors ? { colors } : {}),
|
||||||
deckRev: 17,
|
deckRev: 18,
|
||||||
};
|
};
|
||||||
const { state, events } = createGame(config);
|
const { state, events } = createGame(config);
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
|
|||||||
Reference in New Issue
Block a user