diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 79519c5..457edd3 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -2105,23 +2105,39 @@ const CARD_EFFECTS: Record }, redirection: { 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) => { const a = cmd.params?.cell; 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 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)); if (!wa || !wb || wa === wb) return "pick two different outer exits"; - // 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 }; + if ((state.config.deckRev ?? 1) >= 9) { + if (cellKey(wa.to.cell) === cellKey(wb.from.cell)) return "those exits already connect"; + const pa = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(wa.to.cell)); + const pb = state.board.warps.find((w) => cellKey(w.from.cell) === cellKey(wb.to.cell)); + // The chosen exits join (A tokens); their old partners join (B tokens). + wa.to = { cell: { ...wb.from.cell }, side: wb.from.side }; + wb.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 }); return null; diff --git a/packages/engine/test/casting.test.ts b/packages/engine/test/casting.test.ts index 38d539c..e7c3e32 100644 --- a/packages/engine/test/casting.test.ts +++ b/packages/engine/test/casting.test.ts @@ -769,3 +769,28 @@ describe("rules revision 8", () => { 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)); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 7fd202c..6aa416f 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -46,7 +46,7 @@ export interface Room { const rooms = new Map(); /** 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"; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 1fc85db..a32f0f7 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1282,7 +1282,9 @@ {#if selectedCard?.cardId === "boobytrap"} — place 4 tokens ({trapCells.length}/4; the first is real) {/if} - {#if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} + {#if selectedCard?.cardId === "redirection"} + {tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"} + {:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} {tradeFrom ? "— now the second square" : "— click the first square"} {/if} {#if selectedCard?.cardId === "rotate-sector"} diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index c17aa5a..7379ea8 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -132,7 +132,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 8, + deckRev: 9, }; const { state, events } = createGame(config); this.config = config;