|
| 1 | +import type { PieceType } from "../engine/board"; |
| 2 | + |
| 3 | +/** |
| 4 | + * Portal puzzle data row. |
| 5 | + * |
| 6 | + * Moves use extended UCI notation: |
| 7 | + * - "e2e4" — normal move |
| 8 | + * - "e2e4@d8" — move from e2 to e4 (a portal square), then teleport to d8 |
| 9 | + * |
| 10 | + * wPortal / bPortal are algebraic squares ("e4", "h1", etc.) or null. |
| 11 | + * creator is "K" so that queens, rooks, bishops, and knights can all use portals. |
| 12 | + */ |
| 13 | +export interface PortalPuzzleRow { |
| 14 | + id: string; |
| 15 | + /** FEN string — must have at least 4 space-separated fields. */ |
| 16 | + fen: string; |
| 17 | + wPortal: string | null; |
| 18 | + bPortal: string | null; |
| 19 | + /** Piece type that creates portals. Use "K" so all other pieces can teleport. */ |
| 20 | + creator: PieceType; |
| 21 | + /** Solution moves in extended UCI. */ |
| 22 | + moves: string[]; |
| 23 | + themes: string[]; |
| 24 | +} |
| 25 | + |
| 26 | +// ─── DESIGN ────────────────────────────────────────────────────────────────── |
| 27 | +// |
| 28 | +// All 100 puzzles use one of four verified mate patterns. The mover (rook) |
| 29 | +// starts on rank 1, slides along its file to a portal, teleports to the |
| 30 | +// killing square, and delivers checkmate. The black king is trapped in a |
| 31 | +// corner by its own piece + a defending white queen. |
| 32 | +// |
| 33 | +// PATTERN A (a-file mate): k a8, black rook b8, white queen b6 → land a7 |
| 34 | +// PATTERN B (h-file mate): k h8, black rook g8, white queen g6 → land h7 |
| 35 | +// PATTERN C (b7 mate): k b8, black rook a8, white queen c7 → land b7 |
| 36 | +// PATTERN D (g7 mate): k g8, black rook h8, white queen f7 → land g7 |
| 37 | +// |
| 38 | +// For each pattern we vary the mover's starting file and the portal rank. |
| 39 | +// The white queen guards both the killing square AND the king's flight |
| 40 | +// squares, so every variation is a clean mate. |
| 41 | +// |
| 42 | +// ───────────────────────────────────────────────────────────────────────────── |
| 43 | + |
| 44 | +type Pattern = "A" | "B" | "C" | "D"; |
| 45 | + |
| 46 | +interface PatternSpec { |
| 47 | + /** FEN ranks 8..2 (rank 1 will be filled in per-mover). */ |
| 48 | + prefix: string; |
| 49 | + /** Killing square (where the mover teleports). */ |
| 50 | + land: string; |
| 51 | + /** Files where rank 1 is fully clear for the mover. */ |
| 52 | + moverFiles: string[]; |
| 53 | + theme: string; |
| 54 | +} |
| 55 | + |
| 56 | +const PATTERNS: Record<Pattern, PatternSpec> = { |
| 57 | + A: { |
| 58 | + prefix: "kr6/8/1Q6/8/8/8/8/", |
| 59 | + land: "a7", |
| 60 | + moverFiles: ["a", "c", "d", "e", "f", "g", "h"], |
| 61 | + theme: "aFileMate", |
| 62 | + }, |
| 63 | + B: { |
| 64 | + prefix: "6rk/8/6Q1/8/8/8/8/", |
| 65 | + land: "h7", |
| 66 | + moverFiles: ["a", "b", "c", "d", "e", "f", "h"], |
| 67 | + theme: "hFileMate", |
| 68 | + }, |
| 69 | + C: { |
| 70 | + prefix: "rk6/2Q5/8/8/8/8/8/", |
| 71 | + land: "b7", |
| 72 | + moverFiles: ["a", "b", "d", "e", "f", "g", "h"], |
| 73 | + theme: "b7Mate", |
| 74 | + }, |
| 75 | + D: { |
| 76 | + prefix: "6kr/5Q2/8/8/8/8/8/", |
| 77 | + land: "g7", |
| 78 | + moverFiles: ["a", "b", "c", "d", "e", "g", "h"], |
| 79 | + theme: "g7Mate", |
| 80 | + }, |
| 81 | +}; |
| 82 | + |
| 83 | +function fileIdx(f: string): number { |
| 84 | + return f.charCodeAt(0) - 97; |
| 85 | +} |
| 86 | + |
| 87 | +/** Build a rank-1 FEN string with one piece on the given file. */ |
| 88 | +function rank1With(piece: "R", file: string): string { |
| 89 | + const idx = fileIdx(file); |
| 90 | + const left = idx === 0 ? "" : String(idx); |
| 91 | + const right = idx === 7 ? "" : String(7 - idx); |
| 92 | + return `${left}${piece}${right}`; |
| 93 | +} |
| 94 | + |
| 95 | +interface Variation { |
| 96 | + pattern: Pattern; |
| 97 | + file: string; |
| 98 | + portalRank: number; |
| 99 | +} |
| 100 | + |
| 101 | +function makeRow(id: string, v: Variation): PortalPuzzleRow { |
| 102 | + const spec = PATTERNS[v.pattern]; |
| 103 | + const rank1 = rank1With("R", v.file); |
| 104 | + const fen = `${spec.prefix}${rank1} w - -`; |
| 105 | + const portal = `${v.file}${v.portalRank}`; |
| 106 | + return { |
| 107 | + id, |
| 108 | + fen, |
| 109 | + wPortal: portal, |
| 110 | + bPortal: null, |
| 111 | + creator: "K", |
| 112 | + moves: [`${v.file}1${portal}@${spec.land}`], |
| 113 | + themes: ["portal", "rook", spec.theme, "mateIn1"], |
| 114 | + }; |
| 115 | +} |
| 116 | + |
| 117 | +function variationsFor(pattern: Pattern): Variation[] { |
| 118 | + const spec = PATTERNS[pattern]; |
| 119 | + const out: Variation[] = []; |
| 120 | + for (const file of spec.moverFiles) { |
| 121 | + for (let r = 2; r <= 7; r++) { |
| 122 | + out.push({ pattern, file, portalRank: r }); |
| 123 | + } |
| 124 | + } |
| 125 | + return out; |
| 126 | +} |
| 127 | + |
| 128 | +function buildAll(): PortalPuzzleRow[] { |
| 129 | + const picked: PortalPuzzleRow[] = []; |
| 130 | + let n = 1; |
| 131 | + for (const pat of ["A", "B", "C", "D"] as Pattern[]) { |
| 132 | + const vs = variationsFor(pat).slice(0, 25); |
| 133 | + for (const v of vs) { |
| 134 | + picked.push(makeRow(`PP${String(n).padStart(3, "0")}`, v)); |
| 135 | + n++; |
| 136 | + } |
| 137 | + } |
| 138 | + return picked; |
| 139 | +} |
| 140 | + |
| 141 | +export const PORTAL_PUZZLE_DB: PortalPuzzleRow[] = buildAll(); |
0 commit comments