Rev 11: the maze has no wall at zero, and moving sectors carry everything
Relocate Sector could never land a sector west or north of the origin — placements the physical game allows by simply sliding the whole map across the table. The coordinate grid still cannot go negative, but now it does not need to: a sector may land at negative coordinates and the whole maze renormalizes, zero-anchoring on both axes (which also pulls the maze snug when the origin corner is vacated, instead of leaving a blank band). The client offers the new landings as ghost slots on every side, verified slot-for-slot against engine truth. The move also closes a real gap: remapState never carried creatures, boobytrap tokens, glue, open safes, or dimensional warp tokens when a sector relocated or rotated — they were left hovering at their old coordinates. Sectors now carry everything standing on them. Both changes ride rules rev 11; older ledgers replay their flaws intact, as replay determinism demands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
53c84ec6cb
commit
73150a89b5
@@ -2806,6 +2806,24 @@ function remapState(
|
||||
for (const t of state.treasures) {
|
||||
if (t.position && inSector(t.position)) t.position = mapCell(t.position);
|
||||
}
|
||||
// A moving sector carries everything standing on it. Earlier revisions left
|
||||
// creatures, traps, glue, safes, and warp tokens at their old coordinates;
|
||||
// their stored games must replay with that flaw intact (rules rev 11).
|
||||
if ((state.config.deckRev ?? 1) >= 11) {
|
||||
for (const c of state.creatures) {
|
||||
if (inSector(c.position)) c.position = mapCell(c.position);
|
||||
}
|
||||
for (const t of state.boobytraps) {
|
||||
t.cells = t.cells.map((c) => (inSector(c) ? mapCell(c) : c));
|
||||
t.realKey = mapCellKey(t.realKey);
|
||||
}
|
||||
state.gluedCells = remapRecord(state.gluedCells, mapCellKey);
|
||||
state.openSafes = state.openSafes.map(mapCellKey);
|
||||
for (const w of state.dimWarps) {
|
||||
if (inSector(w.a)) w.a = mapCell(w.a);
|
||||
if (inSector(w.b)) w.b = mapCell(w.b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ROTATE SECTOR: 90 degrees, pieces and alterations turning with it. */
|
||||
@@ -2839,7 +2857,11 @@ function relocateSector(state: GameState, index: number, dest: Cell): string | n
|
||||
const placements = state.board.placements;
|
||||
const current = placements[index]!.origin;
|
||||
if (dest.x === current.x && dest.y === current.y) return "the sector is already there";
|
||||
if (dest.x < 0 || dest.y < 0) return "the sector cannot go there";
|
||||
// The grid has no wall at zero: a sector may land beyond the old origin and
|
||||
// the whole maze renormalizes back into positive coordinates (rules rev 11).
|
||||
if ((state.config.deckRev ?? 1) < 11 && (dest.x < 0 || dest.y < 0)) {
|
||||
return "the sector cannot go there";
|
||||
}
|
||||
for (let i = 0; i < placements.length; i++) {
|
||||
if (i === index) continue;
|
||||
const o = placements[i]!.origin;
|
||||
@@ -2861,9 +2883,24 @@ function relocateSector(state: GameState, index: number, dest: Cell): string | n
|
||||
const mapCell = (c: Cell): Cell => ({ x: c.x + dx, y: c.y + dy });
|
||||
|
||||
const newPlacements = placements.map((p, i) => (i === index ? { ...p, origin: dest } : p));
|
||||
// Zero-anchor the maze after the move: negative landings shift back into
|
||||
// positive coordinates, and vacating the origin corner pulls the maze snug
|
||||
// instead of leaving a blank band (rules rev 11; older games never shift).
|
||||
const shift = (state.config.deckRev ?? 1) >= 11
|
||||
? {
|
||||
x: -Math.min(...newPlacements.map((p) => p.origin.x)),
|
||||
y: -Math.min(...newPlacements.map((p) => p.origin.y)),
|
||||
}
|
||||
: { x: 0, y: 0 };
|
||||
const shifted = shift.x || shift.y
|
||||
? newPlacements.map((p) => ({ ...p, origin: { x: p.origin.x + shift.x, y: p.origin.y + shift.y } }))
|
||||
: newPlacements;
|
||||
// "Only opposite board edges connect" after a relocation: default pairings.
|
||||
state.board = assembleBoard(newPlacements);
|
||||
state.board = assembleBoard(shifted);
|
||||
remapState(state, inSector, mapCell, (s) => s);
|
||||
if (shift.x || shift.y) {
|
||||
remapState(state, () => true, (c) => ({ x: c.x + shift.x, y: c.y + shift.y }), (s) => s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface GameView {
|
||||
winReason: "treasures" | "lastStanding" | null;
|
||||
turn: TurnState;
|
||||
activePlayerId: PlayerId;
|
||||
/** The game's rules revision — the client mirrors rev-gated legality. */
|
||||
deckRev: number;
|
||||
/** Board with dynamic wall changes already merged in. */
|
||||
board: AssembledBoard;
|
||||
players: PlayerPublicView[];
|
||||
@@ -120,6 +122,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
||||
revealedHands: state.phase === "finished"
|
||||
? Object.fromEntries(state.players.map((p) => [p.id, p.alive ? [...p.hand] : [...(p.finalHand ?? p.hand)]]))
|
||||
: null,
|
||||
deckRev: state.config.deckRev ?? 1,
|
||||
treasures: state.treasures.map((t) => ({ ...t })),
|
||||
deckCount: state.deck.length,
|
||||
discardCount: state.discard.length,
|
||||
|
||||
Reference in New Issue
Block a user