Rev 9: REDIRECTION connects the exits you choose
Playtesting read the card right and the engine had it wrong. "Placing the A tokens on one set of exits & the B tokens on the other" means the two exits you mark BECOME a pair — and their former partners pair with each other — not that the two exits trade destinations. Under rev 9, click the exit and then the exit you want it to connect TO (the hint now says so); already-connected exits are refused. Earlier games keep the swap their logs were validated against. Test pins the full four-way rewiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d9922da4df
commit
2c3e622ce5
+26
-10
@@ -2105,23 +2105,39 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
},
|
},
|
||||||
redirection: {
|
redirection: {
|
||||||
kind: "neutral",
|
kind: "neutral",
|
||||||
// "Swap two external sector exits" — their wraparound destinations trade.
|
// "Signify their new directions by placing the A tokens on one set of
|
||||||
|
// exits & the B tokens on the other" — the two exits you choose become a
|
||||||
|
// pair (rules rev 9), and their former partners pair with each other.
|
||||||
|
// Earlier revisions swapped the two exits' destinations instead.
|
||||||
resolve: (state, events, caster, cmd) => {
|
resolve: (state, events, caster, cmd) => {
|
||||||
const a = cmd.params?.cell;
|
const a = cmd.params?.cell;
|
||||||
const bT = cmd.target;
|
const bT = cmd.target;
|
||||||
if (!a || !bT || bT.kind !== "cell") return "pick the two exits to swap";
|
if (!a || !bT || bT.kind !== "cell") return "pick the two exits to connect";
|
||||||
const b = bT.cell;
|
const b = bT.cell;
|
||||||
const wa = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(a));
|
const wa = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(a));
|
||||||
const wb = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(b));
|
const wb = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(b));
|
||||||
if (!wa || !wb || wa === wb) return "pick two different outer exits";
|
if (!wa || !wb || wa === wb) return "pick two different outer exits";
|
||||||
// Swap destinations and fix the reciprocal warps to match.
|
if ((state.config.deckRev ?? 1) >= 9) {
|
||||||
const destA = { ...wa.to };
|
if (cellKey(wa.to.cell) === cellKey(wb.from.cell)) return "those exits already connect";
|
||||||
const destB = { ...wb.to };
|
const pa = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(wa.to.cell));
|
||||||
wa.to = destB;
|
const pb = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(wb.to.cell));
|
||||||
wb.to = destA;
|
// The chosen exits join (A tokens); their old partners join (B tokens).
|
||||||
for (const w of state.board.warps) {
|
wa.to = { cell: { ...wb.from.cell }, side: wb.from.side };
|
||||||
if (cellKey(w.from.cell) === cellKey(destA.cell)) w.to = { cell: { ...wb.from.cell }, side: wb.from.side };
|
wb.to = { cell: { ...wa.from.cell }, side: wa.from.side };
|
||||||
if (cellKey(w.from.cell) === cellKey(destB.cell)) w.to = { cell: { ...wa.from.cell }, side: wa.from.side };
|
if (pa && pb) {
|
||||||
|
pa.to = { cell: { ...pb.from.cell }, side: pb.from.side };
|
||||||
|
pb.to = { cell: { ...pa.from.cell }, side: pa.from.side };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy: swap destinations and fix the reciprocal warps to match.
|
||||||
|
const destA = { ...wa.to };
|
||||||
|
const destB = { ...wb.to };
|
||||||
|
wa.to = destB;
|
||||||
|
wb.to = destA;
|
||||||
|
for (const w of state.board.warps) {
|
||||||
|
if (cellKey(w.from.cell) === cellKey(destA.cell)) w.to = { cell: { ...wb.from.cell }, side: wb.from.side };
|
||||||
|
if (cellKey(w.from.cell) === cellKey(destB.cell)) w.to = { cell: { ...wa.from.cell }, side: wa.from.side };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
events.push({ type: "exitsRedirected", caster: caster.id });
|
events.push({ type: "exitsRedirected", caster: caster.id });
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -769,3 +769,28 @@ describe("rules revision 8", () => {
|
|||||||
if (!r.ok) expect(r.error).toMatch(/warp shimmers/);
|
if (!r.ok) expect(r.error).toMatch(/warp shimmers/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("redirection (rules rev 9)", () => {
|
||||||
|
it("the two chosen exits connect; their old partners pair with each other", () => {
|
||||||
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 9 });
|
||||||
|
state = toRound2(state);
|
||||||
|
const me = activePlayer(state);
|
||||||
|
// Pick two exits that are NOT already partners.
|
||||||
|
const warps = state.board.warps;
|
||||||
|
const wa = warps[0]!;
|
||||||
|
const wb = warps.find((w) => cellKey(w.from.cell) !== cellKey(wa.from.cell) &&
|
||||||
|
cellKey(w.from.cell) !== cellKey(wa.to.cell))!;
|
||||||
|
const paCell = { ...wa.to.cell };
|
||||||
|
const pbCell = { ...wb.to.cell };
|
||||||
|
const rd = giveCard(state, me.id, "redirection", "RD", 0);
|
||||||
|
state = must(state, me.id, {
|
||||||
|
type: "cast", instanceId: rd.instanceId,
|
||||||
|
target: { kind: "cell", cell: wb.from.cell }, params: { cell: wa.from.cell },
|
||||||
|
});
|
||||||
|
const after = (from: Cell) => state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(from))!;
|
||||||
|
expect(cellKey(after(wa.from.cell).to.cell)).toBe(cellKey(wb.from.cell));
|
||||||
|
expect(cellKey(after(wb.from.cell).to.cell)).toBe(cellKey(wa.from.cell));
|
||||||
|
expect(cellKey(after(paCell).to.cell)).toBe(cellKey(pbCell));
|
||||||
|
expect(cellKey(after(pbCell).to.cell)).toBe(cellKey(paCell));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,7 +46,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 = 8;
|
const RULES_REV = 9;
|
||||||
|
|
||||||
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
|
|
||||||
|
|||||||
@@ -1282,7 +1282,9 @@
|
|||||||
{#if selectedCard?.cardId === "boobytrap"}
|
{#if selectedCard?.cardId === "boobytrap"}
|
||||||
<span>— place 4 tokens ({trapCells.length}/4; the first is real)</span>
|
<span>— place 4 tokens ({trapCells.length}/4; the first is real)</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
|
{#if selectedCard?.cardId === "redirection"}
|
||||||
|
<span>{tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"}</span>
|
||||||
|
{:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
|
||||||
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
|
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard?.cardId === "rotate-sector"}
|
{#if selectedCard?.cardId === "rotate-sector"}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class LocalGame {
|
|||||||
seed,
|
seed,
|
||||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||||
...(colors ? { colors } : {}),
|
...(colors ? { colors } : {}),
|
||||||
deckRev: 8,
|
deckRev: 9,
|
||||||
};
|
};
|
||||||
const { state, events } = createGame(config);
|
const { state, events } = createGame(config);
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|||||||
Reference in New Issue
Block a user