4D chess: quality of life improvements
| <!doctype html> | |
| <html lang="en" data-input="pointer"> | |
| <head> | |
| 4D chess | |
| *{box-sizing:border-box}body{margin:0;background:#fafbf9;color:#24342b;font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}button,select,summary{font:inherit;color:inherit}button,summary{cursor:pointer}button,select{min-height:38px;border:1px solid #cdd3cb;border-radius:5px;background:#fff;padding:7px 12px}button:hover:not(:disabled),summary:hover{background:#edf0e9}button:disabled{opacity:.4;cursor:default}button:focus-visible,select:focus-visible,summary:focus-visible{outline:3px solid #bc7131;outline-offset:2px}[hidden]{display:none!important}h1,h2,h3,p{margin:0}h1{font-size:18px;font-weight:650}h2{font-size:16px;font-weight:600}h3{font-size:14px}main{max-width:1800px;margin:auto;padding:24px 32px}.toolbar{display:flex;gap:12px;align-items:center;border-bottom:1px solid #dce0d8;padding-bottom:16px;margin-bottom:20px}.toolbar h1{margin-right:auto;white-space:nowrap}.toolbar select{max-width:210px;font-size:13px}.game-menu{position:relative}.game-menu>summary{list-style:none;min-height:38px;border:1px solid #cdd3cb;border-radius:5px;padding:7px 12px;background:white}.game-menu>summary::-webkit-details-marker{display:none}.menu-items{position:absolute;z-index:5;right:0;top:44px;width:210px;border:1px solid #cdd3cb;border-radius:6px;padding:5px;background:white;box-shadow:0 5px 16px #0001}.menu-items button{display:block;width:100%;text-align:left;border:0;font-size:13px}.workspace{display:grid;grid-template-columns:minmax(0,1fr);justify-content:center;gap:clamp(44px,4vw,80px)}.game{min-width:0}.game-state{display:flex;align-items:center;gap:9px;min-height:38px;margin-bottom:12px}.turn-token{width:15px;height:15px;background:#fffbef;border:1.5px solid #53604f;border-radius:50%;flex-shrink:0}.turn-token.black{background:#284739}.status-copy{margin-right:auto}.status-copy p{font-size:12px;color:#974329}.undo{font-size:13px}.slice-grid{display:grid;grid-template-columns:20px 1fr 1fr;gap:14px 18px}.z-label{text-align:center;font-size:12px;color:#546251}.axis-w{writing-mode:vertical-rl;transform:rotate(180deg);text-align:center;font-size:12px;color:#546251}.slice{min-width:0}.board-wrap{position:relative;padding-left:13px}.ranks{position:absolute;left:0;top:0;bottom:0;display:grid;grid-template-rows:repeat(4,1fr);align-items:center;font-size:10px;color:#62705e}.board{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));grid-template-rows:repeat(4,minmax(0,1fr));aspect-ratio:1;border:1px solid #98a88f}.cell{position:relative;display:flex;align-items:center;justify-content:center;aspect-ratio:1;min-height:0;min-width:0;border:0;border-radius:0;padding:0;background:#e7ebdf}.cell.dark{background:#a9bda0}.cell:hover:not(:disabled){background:#c7d3ba}.cell.last{background:#d2d7a5}.cell.last.dark{background:#b9c48d}.cell.selected,.cell.selected:hover{background:#e8c27d}.cell.legal:after{content:'';position:absolute;width:20%;height:20%;border-radius:50%;background:#2d603ba6;pointer-events:none}.cell.capture:after{width:87%;height:87%;background:none;border:3px solid #ac4e2e}.cell.checked{background:#dfa893}.cell.hint,.cell:focus-visible{outline:3px solid #b7672c;outline-offset:-3px;z-index:1}.piece{width:76%;height:76%;max-width:72px;overflow:visible;pointer-events:none}.piece.white{color:#fff9e8;--stroke:#405344;--detail:#405344}.piece.black{color:#284739;--stroke:#1c3228;--detail:#bed0b6}.files{display:grid;grid-template-columns:repeat(4,1fr);padding:4px 0 0 13px;text-align:center;font-size:10px;color:#62705e}.inspector{margin:16px 0 0 33px;border-top:1px solid #dce0d8;padding-top:12px}.inspector-head{display:flex;gap:12px;align-items:center;justify-content:space-between}.move-count{font-size:12px;color:#546251}.inspector p{font-size:12px;margin-top:5px;color:#546251;line-height:1.6}.coordinates{display:flex;gap:14px;margin-top:8px;font:12px ui-monospace,monospace}.coord.changed{color:#984421;font-weight:650}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}dialog{width:min(640px,calc(100% - 24px));max-height:88vh;margin:auto;padding:0;border:1px solid #cdd3cb;border-radius:8px;background:#fafbf9;color:#24342b;box-shadow:0 15px 50px #0003}dialog::backdrop{background:#17251a88}.dialog-content{padding:24px}.dialog-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.dialog-heading h2{font-size:22px}.close{width:38px;padding:0;font-size:21px}.dialog-content p{font-size:13px;line-height:1.7;margin:12px 0;color:#4e5c49}.example{padding:12px 14px;background:#edf0e9;font-size:13px;line-height:1.7}.rules-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:20px 0}.rule strong{font-size:15px}.rule p{margin:4px 0 0;font-size:12px}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}.primary{background:#315c46;color:white;border-color:#315c46}.primary:hover:not(:disabled){background:#264b38}.move-list{max-height:50vh;overflow:auto}.move-entry{display:grid;grid-template-columns:25px 1fr;gap:8px;padding:10px 0;border-bottom:1px solid #dce0d8;font-size:13px}.move-entry strong{font-weight:550}.move-entry small{display:block;color:#546251;font:12px ui-monospace,monospace;margin-top:3px}.move-num,.captured{color:#546251;font-size:12px}.captured{margin-top:16px}.history-count{color:#546251;font-size:12px;margin-bottom:12px} | |
| @media(max-width:700px){main{padding:14px 12px}.toolbar{gap:8px;flex-wrap:wrap;margin-bottom:12px;padding-bottom:12px}.toolbar h1{font-size:17px}.toolbar select{width:128px;font-size:12px}.toolbar button,.toolbar summary{font-size:12px;padding:7px 9px}.workspace{gap:22px}.slice-grid{gap:12px 10px;grid-template-columns:16px 1fr 1fr}.inspector{margin-left:29px}.coordinates{gap:10px}.rules-grid{grid-template-columns:1fr}.dialog-content{padding:20px}}@media(max-width:360px){.toolbar select{max-width:128px}.toolbar{gap:5px}.toolbar h1{font-size:15px}.toolbar button,.toolbar summary{padding:7px}.slice-grid{column-gap:7px}} | |
| .workspace{grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;justify-content:stretch} | |
| .spatial{min-width:0;margin-top:8px}.spatial>summary{font-size:14px;font-weight:600;padding:5px 0;width:fit-content}.spatial-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:7px 0 0}.projection-label{font-size:12px;color:#5b6656}.reset-view{font-size:12px;min-height:32px;padding:4px 9px}.space-svg{display:block;width:100%;height:auto;aspect-ratio:1;touch-action:none;cursor:grab;overflow:visible}.space-svg:active{cursor:grabbing}.space-svg:focus-visible{outline:2px solid #bc7131;outline-offset:2px}.space-svg [data-square]{cursor:pointer}.space-svg .space-white{color:#fff9e8;--stroke:#405344;--detail:#405344}.space-svg .space-black{color:#284739;--stroke:#1c3228;--detail:#bed0b6}.space-svg .node-hit{fill:transparent}.space-svg [data-square]:hover .node-hit{fill:#bd71351a}html[data-input="keyboard"] .space-svg [data-square]:focus-visible .node-hit{fill:#bd71352b;stroke:#af602c;stroke-width:1}.space-caption{font-size:12px;color:#5b6656;min-height:36px;margin-top:6px}.space-caption strong{color:#24342b;font-weight:550} | |
| @media(max-width:1000px){.workspace{grid-template-columns:minmax(0,1.12fr) minmax(0,1fr);gap:32px}}@media(max-width:700px){.workspace{grid-template-columns:minmax(0,640px);justify-content:center;gap:28px}.spatial{margin-top:0;border-top:1px solid #dce0d8;padding-top:10px}.space-svg{max-width:600px;margin:auto}.space-caption{min-height:0}.spatial>summary{min-height:38px;display:list-item;align-content:center}} | |
| .learn{max-width:1200px;margin:46px auto 0;border-top:1px solid #cdd3cb;padding-top:26px;scroll-margin-top:20px}.learn a{color:#315c46;text-underline-offset:3px}.learn-heading{display:flex;align-items:center;justify-content:space-between;gap:20px}.learn-heading h2{font-size:24px;line-height:1.3}.learn-heading a{font-size:13px;white-space:nowrap}.learn-lead{max-width:760px;margin-top:13px;color:#4e5c49;line-height:1.8}.learn-section{margin-top:30px}.learn-section>h3,.learn-finish h3{font-size:17px;margin-bottom:12px}.learn-section>p{color:#4e5c49;max-width:800px;line-height:1.8}.learn-intro-grid,.learn-finish{display:grid;grid-template-columns:1.25fr 1fr;gap:40px}.learn-prose p{line-height:1.8;color:#4e5c49;margin-bottom:12px}.learn-prose strong{font-weight:600;color:#24342b}.learn code{font:12px ui-monospace,monospace;color:#24342b;white-space:nowrap}.axis-table,.learn-rule-table{border-collapse:collapse;width:100%;font-size:13px;text-align:left}.axis-table caption{text-align:left;font-size:13px;font-weight:600;margin-bottom:8px}.axis-table th,.axis-table td,.learn-rule-table th,.learn-rule-table td{padding:9px 10px;border-bottom:1px solid #dce0d8;vertical-align:top;line-height:1.7}.axis-table thead,.learn-rule-table thead{background:#edf0e9;font-size:12px}.axis-table td,.learn-rule-table td{color:#4e5c49}.axis-table tbody th{width:40px}.address-example{font-size:12px;color:#4e5c49;line-height:1.7;margin-top:12px}.lesson{border-top:1px solid #dce0d8;border-bottom:1px solid #dce0d8;padding:24px 0}.lesson-intro{font-size:13px}.lesson-pieces{display:flex;flex-wrap:wrap;gap:7px;margin-top:17px}.lesson-pieces button{font-size:13px;min-width:75px;min-height:40px}.lesson-pieces button[aria-pressed=true]{border-color:#315c46;background:#315c46;color:white}.lesson-options{display:flex;gap:12px;align-items:center;justify-content:space-between;margin:15px 0}.lesson-options fieldset{padding:0;margin:0;border:0;display:flex;gap:22px}.lesson-options label{font-size:13px;display:flex;align-items:center;gap:6px;cursor:pointer;min-height:38px}.lesson-options input{accent-color:#315c46;width:16px;height:16px;margin:0}.lesson-options button{font-size:12px}.lesson-explanation{padding:13px 16px;background:#edf0e9;min-height:85px;border-left:3px solid #668659}.lesson-explanation h4{font-size:15px;margin:0 0 5px}.lesson-explanation p{font-size:13px;color:#4e5c49;line-height:1.7}.lesson[data-mistake=true] .lesson-explanation{background:#f5e8df;border-left-color:#ac4e2e}.lesson-comparison{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:20px}.lesson figure{min-width:0;margin:0}.lesson-comparison figure+figure{border-left:1px solid #dce0d8;padding-left:24px}.lesson-comparison figcaption{display:flex;justify-content:space-between;gap:10px;align-items:baseline;font-size:13px}.lesson-comparison figcaption strong{font-weight:600}.lesson-comparison figcaption span{color:#526a46;font:12px ui-monospace,monospace}.lesson-svg{display:block;width:100%;max-height:320px;aspect-ratio:6/5;touch-action:none;cursor:grab}.lesson-svg:active{cursor:grabbing}.lesson-svg:focus-visible{outline:2px solid #bc7131;outline-offset:2px}.lesson-svg .lesson-white,#lesson-flat .lesson-white{color:#fff9e8;--stroke:#405344;--detail:#405344}.lesson-svg .lesson-black,#lesson-flat .lesson-black{color:#284739;--stroke:#1c3228;--detail:#bed0b6}.lesson-comparison figure>p{font-size:13px;line-height:1.7;color:#4e5c49;min-height:45px}.lesson-coordinate{display:flex;flex-wrap:wrap;align-items:center;gap:9px;font:12px/1.8 ui-monospace,monospace;margin-top:8px}.lesson-coordinate .axis-change{font-size:11px;color:#526a46}.lesson-coordinate .axis-change b{color:#984421;font-weight:600}.lesson-playback{display:flex;gap:15px;align-items:center;margin-top:20px}.lesson-playback button{min-width:110px;font-size:13px}.lesson-playback input{accent-color:#315c46;flex:1;min-width:30px;height:38px;cursor:pointer}.lesson-playback output{font-size:12px;min-width:46px;color:#546251;text-align:right}.lesson-view-help{font-size:12px!important;margin-top:10px;color:#62705e!important}.lesson-flat-row{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:center;margin-top:24px;padding-top:20px;border-top:1px solid #dce0d8}.lesson-flat-row figcaption{font-size:12px;font-weight:600;margin-bottom:7px}.lesson-flat-row svg{width:100%;display:block}.lesson-flat-row h4{font-size:15px;margin:0 0 8px}.lesson-flat-row p{font-size:13px}.lesson-isolation{font-size:12px!important;color:#62705e!important}.learn-rule-table{margin-top:16px}.learn-rule-table th:first-child{width:85px}.learn-rule-table td{width:46%}.learn-small{font-size:12px;margin-top:12px}.learn-finish{margin-top:28px}.learn-return{display:inline-block;margin:0 0 12px;font-size:13px;min-height:38px;padding:8px 0}.learn-extra{border-top:1px solid #dce0d8;padding-top:10px;margin-top:5px}.learn-extra summary{font-size:13px;min-height:38px;align-content:center}.learn-extra p{font-size:12px;margin:8px 0 0} | |
| @media(max-width:760px){.learn-intro-grid,.learn-finish{grid-template-columns:1fr;gap:18px}.learn-heading h2{font-size:21px}.learn-heading a{display:none}.lesson-comparison{gap:16px}.lesson-comparison figure+figure{padding-left:16px}.lesson-comparison figcaption{align-items:start;flex-direction:column;gap:2px}.lesson-comparison figure>p{min-height:0}.lesson-flat-row{grid-template-columns:210px 1fr;gap:20px}} | |
| @media(max-width:520px){.learn{margin-top:22px;padding-top:22px}.learn-section{margin-top:23px}.learn-lead,.learn-prose p,.learn-section>p{font-size:13px}.lesson-pieces{display:grid;grid-template-columns:repeat(3,1fr)}.lesson-pieces button{min-width:0}.lesson-options{flex-wrap:wrap;gap:4px}.lesson-options fieldset{gap:14px}.lesson-options label{font-size:12px}.lesson-options button{min-height:34px;padding:5px 8px}.lesson-comparison{grid-template-columns:1fr;gap:22px}.lesson-comparison figure+figure{border-left:0;border-top:1px solid #dce0d8;padding-left:0;padding-top:17px}.lesson-comparison figcaption{flex-direction:row;align-items:baseline}.lesson-svg{max-height:280px}.lesson-flat-row{grid-template-columns:1fr;gap:15px}.lesson-flat-row figure{width:250px;max-width:100%;justify-self:center}.lesson-playback{gap:10px}.lesson-playback button{min-width:100px}.learn-rule-table{font-size:12px}.learn-rule-table th,.learn-rule-table td{padding:8px 6px}.learn-rule-table th:first-child{width:52px}.learn-rule-table td{width:auto}.learn-rule-table thead{display:none}.learn-rule-table tr{display:grid;grid-template-columns:58px minmax(0,1fr);border-bottom:1px solid #dce0d8}.learn-rule-table tbody th{grid-row:span 2;border:0}.learn-rule-table td{grid-column:2;border:0}.learn-rule-table td:last-child{padding-top:0;font-size:11px}.lesson-explanation{min-height:0}} | |
| /* Pointer interaction has no focus ring; keyboard navigation stays visible. */ | |
| html[data-input="pointer"] :focus{outline:none!important} | |
| html[data-input="keyboard"] :is(button,select,summary,input,a,[tabindex]):focus-visible{outline:2px solid #bc7131;outline-offset:3px} | |
| html[data-input="keyboard"] .cell:focus-visible{outline-offset:-3px} | |
| :is(button,select,summary,input,a,.space-svg,.lesson-svg){-webkit-tap-highlight-color:transparent} | |
| button:not(.cell){display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:40px;padding:10px 14px;line-height:1.2;text-align:center} | |
| select{appearance:none;-webkit-appearance:none;min-height:40px;padding:10px 32px 10px 12px;line-height:1.2;text-align:center;text-align-last:center;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='%23546251' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 11px center;background-size:12px} | |
| select option{text-align:left} | |
| .game-menu>summary{display:flex;align-items:center;justify-content:center;width:40px;min-height:40px;padding:0;line-height:1;font-size:20px} | |
| .menu-items{top:46px} | |
| .menu-items button{justify-content:flex-start;text-align:left;padding:11px 12px} | |
| button.close{width:40px;min-width:40px;padding:0;flex-shrink:0} | |
| .lesson-options label{min-height:40px} | |
| @media(max-width:700px){.toolbar select{width:134px}.toolbar>button{padding:10px 12px}.toolbar .game-menu>summary{padding:0;font-size:20px}} | |
| @media(max-width:360px){.toolbar{gap:4px}.toolbar select{width:120px;max-width:120px;padding-left:8px;padding-right:26px;background-position:right 8px center}.toolbar>button{padding:10px 8px}.toolbar .game-menu>summary{width:36px}.lesson-options button{padding:10px 12px}} | |
| /* A single overlay lets a piece cross between any of the four flat boards. */ | |
| .slice-grid{position:relative} | |
| #board-motion{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:3} | |
| .moving-piece.white{color:#fff9e8;--stroke:#405344;--detail:#405344} | |
| .moving-piece.black{color:#284739;--stroke:#1c3228;--detail:#bed0b6} | |
| .slice-grid[aria-busy="true"] .cell{cursor:progress} | |
| .status-copy p{color:#546251} | |
| .source-link{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:40px;height:40px;border:1px solid #cdd3cb;border-radius:5px;background:#fff;color:#24342b;text-decoration:none} | |
| .source-link:hover{background:#edf0e9} | |
| .source-link svg{display:block;pointer-events:none} | |
| .site-footer{max-width:1200px;margin:28px auto 0;padding:18px 0 6px;border-top:1px solid #dce0d8;text-align:center;font-size:13px} | |
| .site-footer a{color:#546251;text-underline-offset:3px} | |
| .site-footer a:hover{color:#24342b} | |
| @media(max-width:480px){ | |
| .toolbar{display:grid;grid-template-columns:minmax(0,1fr) 40px auto;column-gap:8px;row-gap:10px} | |
| .toolbar h1{grid-column:1;grid-row:1} | |
| .toolbar .source-link{grid-column:2;grid-row:1} | |
| .toolbar .game-menu{grid-column:3;grid-row:1;justify-self:end} | |
| .toolbar select{grid-column:1 / span 2;grid-row:2;width:100%;max-width:none} | |
| .toolbar #rules-button{grid-column:3;grid-row:2} | |
| } | |
| #board-threats{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:4} | |
| .cell.threat-attacker{background:#e9c6b9} | |
| .cell.threat-target,.cell.threat-target:hover{background:#c9dce6} | |
| .threat-ghost{opacity:.65} | |
| .cell,#space-svg{-webkit-touch-callout:none;user-select:none;-webkit-user-select:none} | |
| .game-state .status-copy{min-width:0} | |
| .game-state .undo{flex-shrink:0;white-space:nowrap} | |
| .threat-summary{margin:14px 0 0 33px;border-top:1px solid #dce0d8;padding-top:12px} | |
| .threat-summary-head{display:flex;align-items:center;justify-content:space-between;gap:10px} | |
| .threat-summary-head strong{font-size:13px;font-weight:600;color:#365a70} | |
| .threat-summary-head button{min-height:34px;padding:7px 10px;font-size:12px} | |
| .threat-summary #threat-context.threat-warning{color:#ad432c} | |
| .threat-summary p{margin-top:6px;font-size:12px;line-height:1.65;color:#546251} | |
| @media(max-width:700px){.threat-summary{margin-left:29px}} | |
| </style> | |
| </head> | |
| <body> | |
| </defs></svg> | |
4D chessPlay mode⋯ | |
White to move | |
| Z = 0Z = 1 | |
| </section> | |
| 3D view | |
| Tesseract projection | |
W = 0 is the inner cube. W = 1 is the outer cube. | |
| </details> | |
| </div> | |
How to play, from 3D to 4DBack to game ↑ | |
| The goal is still checkmate. White moves first. Select a piece, then a marked square. A solid dot is an empty destination; a ring marks a capture. In computer mode, you play White. | |
1. Give your 3D board one more coordinate | |
| Think of a 3D chessboard as layers of ordinary boards. X and Y locate a square within a layer; Z chooses the layer. Here, each 3D board has two 4 × 4 layers. | |
| Now make a second copy of that whole 3D board. W chooses which copy you are in. Every square has a corresponding square in the other copy. A move along W changes copies while keeping X, Y, and Z the same. | |
| In the tesseract view, W = 0 is the inner cube and W = 1 is the outer cube. Both represent equally sized spaces. The inner cube looks smaller because of the projection. W is another spatial direction; it does not mean time. | |
| </div> | |
| The four coordinates on your screenAxisWhere it takes you | |
| XAcross a board, files a to d. | |
| YUp a board, ranks 1 to 4. | |
| ZBetween the left and right boards in the same row. | |
| WBetween the bottom and top boards in the same column. | |
| </tbody></table> | |
|
| |
| </div> | |
| </div> | |
| </section> | |
2. Compare a move in 3D and 4D | |
| Choose a piece, then play or scrub through its move. These cleared-board examples use this game's rules. Other 3D chess variants may define their pieces differently. | |
| </div> | |
| 4D example Legal moveCommon mistake | |
| </div> | |
| Move progressBefore | |
| Drag either drawing to rotate both. You can also focus a drawing and use the arrow keys. Home resets the views. | |
| These diagrams illustrate movement. In your game, pieces along a sliding path block it, and every move must keep your king safe. | |
| </section> | |
3. Count the coordinates that change | |
| Judge a move by its coordinates. A W move can look diagonal in the drawing and still count as a straight rook move. Rotating the view changes how a move looks, not whether it is legal. | |
| Movement rules for all six piecesPieceMovement in this gameWatch for | |
| RookChange exactly one coordinate by any distance.Changing W and Z together is a diagonal, so a rook cannot do it. | |
| BishopChange exactly two coordinates by equal distances.X + W or Z + W works. X + Y + W changes three axes and does not. | |
| KnightChange one coordinate by two and a different coordinate by one. Jump over pieces.The two-step part must use X or Y. Z and W have only two positions. | |
| QueenChange any nonempty combination of coordinates by equal distances.One step on all four axes is legal. Two on X and one on W is not. | |
| KingChange any nonempty combination of coordinates by one step.Every destination must be safe, including threats from another cube. | |
| PawnAdvance one empty square along Y. Capture one Y step forward plus one step along exactly one of X, Z, or W.White advances toward rank 4; Black toward rank 1. Reaching that rank automatically makes a queen. | |
| </tbody></table></div> | |
| Any bishop or queen move involving Z or W is at most one step long on this board. Those axes only run from 0 to 1. X and Y each have four positions. | |
| </section> | |
4. Look for threats across WA king can be in check from a piece in the other cube. For example, a rook at a1 [0,0] attacks a king at a1 [0,1] along W. Distance on the screen gives you no protection. You may capture the attacker, block a longer sliding attack, or move the king, provided the resulting position is safe. A one-step W attack has no intermediate square to block. Checkmate means your king is attacked and no legal reply escapes it. Two lines crossing in the projection do not necessarily share a square. A piece blocks a sliding move only when its full X, Y, Z, W position lies on the path. Knights jump. | |
Your first W moveFrom the starting position, choose White's rook on a1 in the bottom-left board. Select a1 in the top-left board. The rook goes from In the tesseract, that is a move from the inner cube to its matching point on the outer cube. The rook is still moving along just one axis. Try it on the board ↑ | |
| Setup, special moves, and draws Each side starts with a king, queen, two rooks, a bishop, a knight, and four pawns. There is no castling, en passant, or opening pawn double move. The game draws on stalemate, the third occurrence of the same position with the same side to move, 100 turns without a pawn move or capture, or two bare kings. The king is never captured. | |
| </section> | |
| </section> | |
| source code | |
| </main> | |
RulesWin by checkmating the opposing king. White moves first. Select one of your pieces, then a marked destination. In computer mode, you play White. One space, four coordinates. Each square has an X file, a–d, and a Y rank, 1–4. The two board columns are Z = 0 and Z = 1. The two board rows are W = 1 at the top and W = 0 at the bottom. A square is written as a1 [0,0], with Z and W inside the brackets. Moving to the same square on an adjacent board changes just one axis.Each side starts with a king, queen, two rooks, bishop, knight, and four pawns. The following movement rules apply equally to X, Y, Z, and W. RookMove any distance along exactly one axis. It can travel within a board or to the same square on another board in the same row or column. BishopMove the same distance along exactly two axes. This includes ordinary diagonals and diagonals that cross between boards. QueenMove the same distance along any number of axes, from one to all four. Every changed coordinate must change by the same amount. KingMove one step along any combination of axes. It may cross both Z and W in a single move, but may never enter an attacked square. KnightJump two steps along one axis and one along a different axis. It jumps over pieces. On this board, the two-step part must use X or Y. PawnMove one empty square along Y. White goes toward rank 4, Black toward rank 1. Capture one Y step forward plus one step along exactly one of X, Z, or W. Reaching the last rank automatically promotes to a queen. Blocking and check. All pieces except knights are blocked by pieces along their path. You cannot land on a friendly piece or leave your king in check. Checkmate ends the game; the king is never captured. Pawns have no opening double move. There is no castling or en passant. Stalemate, a third repetition, 100 turns without a pawn move or capture, and two bare kings are draws. Each player's turn counts as one move in the journal. Try a fourth-axis move. At the start, select White's rook on a1 in the bottom-left board. Move to a1 in the top-left board. X, Y, and Z stay the same; only W changes.Keyboard. Tab to a square, then press Enter or Space to select a piece or move. Arrow keys move focus within a board. Alt + arrow keys move focus between boards. Escape clears your selection. | |
Start a fresh game?Your current game and move journal will be cleared. | |
Move history | |
| 'use strict'; | |
| // Track input before focus changes, including focus moved by dialogs and SVG views. | |
| document.addEventListener('pointerdown',()=>{document.documentElement.dataset.input='pointer';},{capture:true,passive:true}); | |
| document.addEventListener('keydown',event=>{ | |
| if(['Tab','ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End','Enter',' '].includes(event.key))document.documentElement.dataset.input='keyboard'; | |
| },true); | |
| // All rules, artwork, styles, and the computer player are included in this file. | |
| const Fourfold = (() => { | |
| const dims=[4,4,2,2], names={p:'Pawn',r:'Rook',n:'Knight',b:'Bishop',q:'Queen',k:'King'}; | |
| const coords=Array.from({length:64},(_,i)=>[i%4,Math.floor(i/4)%4,Math.floor(i/16)%2,Math.floor(i/32)]); | |
| const index=([x,y,z,w])=>x+4*y+16*z+32*w; | |
| const address=i=>{const [x,y,z,w]=coords[i];return `${'abcd'[x]}${y+1} [${z},${w}]`;}; | |
| const paths=coords.map((a,i)=>coords.map((b,j)=>{ | |
| if(i===j)return null;const d=b.map((v,k)=>v-a[k]),nonzero=d.filter(Boolean).map(Math.abs),axes=nonzero.length; | |
| const equal=nonzero.every(v=>v===nonzero[0]);const between=[]; | |
| if(equal)for(let s=1;sv+Math.sign(d[k])*s))); | |
| return {d,axes,equal,between,king:Math.max(...nonzero)===1,knight:axes===2&&nonzero.includes(1)&&nonzero.includes(2)}; | |
| })); | |
| function initial(){const b=Array(64).fill(null);const put=(x,y,z,w,t,c)=>b[index([x,y,z,w])]={t,c}; | |
| ['r','n','k','q'].forEach((t,x)=>put(x,0,0,0,t,'w'));for(let x=0;x<4;x++)put(x,1,0,0,'p','w');put(1,0,1,0,'b','w');put(2,0,1,0,'r','w'); | |
| ['q','k','n','r'].forEach((t,x)=>put(x,3,1,1,t,'b'));for(let x=0;x<4;x++)put(x,2,1,1,'p','b');put(2,3,0,1,'b','b');put(1,3,0,1,'r','b');return b; | |
| } | |
| const other=c=>c==='w'?'b':'w'; | |
| function canReach(b,from,to,attack=false){const p=b[from],path=paths[from][to];if(!p||!path)return false;const target=b[to];if(!attack&&target&&target.c===p.c)return false; | |
| const {d,axes,equal,between,king,knight}=path; | |
| if(p.t==='p'){const dir=p.c==='w'?1:-1;if(d[1]!==dir)return false;const lateral=Math.abs(d[0])+Math.abs(d[2])+Math.abs(d[3]);return attack?lateral===1:(!target&&lateral===0)||(!!target&&target.c!==p.c&&lateral===1);} | |
| if(p.t==='n')return knight; | |
| if(p.t==='k')return king; | |
| if(!equal||(p.t==='r'&&axes!==1)||(p.t==='b'&&axes!==2))return false; | |
| return between.every(i=>!b[i]); | |
| } | |
| function inCheck(b,c){const king=b.findIndex(p=>p&&p.c===c&&p.t==='k');if(king<0)return true;return b.some((p,i)=>p&&p.c!==c&&canReach(b,i,king,true));} | |
| function apply(b,m){const n=b.slice();let p=n[m.from];n[m.from]=null;if(p.t==='p'&&coords[m.to][1]===(p.c==='w'?3:0))p={t:'q',c:p.c};n[m.to]=p;return n;} | |
| function legal(b,c,onlyFrom=null){const moves=[];for(let from=0;from<64;from++){const p=b[from];if(!p||p.c!==c||(onlyFrom!==null&&from!==onlyFrom))continue;for(let to=0;to<64;to++){if(b[to]?.t==='k'||!canReach(b,from,to))continue;const m={from,to};if(!inCheck(apply(b,m),c))moves.push(m);}}return moves;} | |
| const key=(b,c)=>c+b.map(p=>p?p.c+p.t:'--').join(''); | |
| function outcome(b,c,halfmove=0,keys=[]){const moves=legal(b,c),check=inCheck(b,c);if(!moves.length)return check?{type:'mate',winner:other(c)}:{type:'stalemate'};if(b.filter(Boolean).every(p=>p.t==='k'))return {type:'bare'};if(halfmove>=100)return {type:'fifty'};if(keys.filter(k=>k===key(b,c)).length>=3)return {type:'repetition'};return null;} | |
| const values={p:100,n:330,b:360,r:470,q:950,k:0}; | |
| function evaluate(b,c){let score=0;for(let i=0;i<64;i++){const p=b[i];if(!p)continue;const [x,y]=coords[i];let v=values[p.t];if(p.t==='p')v+=(p.c==='w'?y:3-y)*24;else if(p.t!=='k')v+=(3-Math.abs(x-1.5)-Math.abs(y-1.5))*5;score+=p.c===c?v:-v;}return score;} | |
| function ordered(b,moves){return moves.sort((a,d)=>((values[b[d.to]?.t]||0)*10-values[b[d.from].t])-((values[b[a.to]?.t]||0)*10-values[b[a.from].t]));} | |
| function computer(b,c){const moves=ordered(b,legal(b,c));let best=null,bestScore=-Infinity;for(const m of moves){const next=apply(b,m),replies=ordered(next,legal(next,other(c)));let score;if(!replies.length)score=inCheck(next,other(c))?100000:0;else{score=Infinity;for(const reply of replies){const leaf=apply(next,reply);const v=evaluate(leaf,c)+(inCheck(leaf,other(c))?18:0)-(inCheck(leaf,c)?22:0);score=Math.min(score,v);if(scorebestScore){bestScore=score;best=m;}}return best;} | |
| return {dims,names,coords,index,address,initial,other,canReach,inCheck,apply,legal,key,outcome,computer}; | |
| })(); | |
| const $=id=>document.getElementById(id), E=Fourfold; | |
| const descriptions={r:'Moves along one axis at a time. Look for the same square on a neighboring board.',b:'Moves equally along exactly two axes. A diagonal can cross between boards.',n:'Jumps two steps along one axis and one along another. Pieces in between do not block it.',q:'Moves equally along one, two, three, or all four axes. Pieces along the path block it.',k:'Moves one step along any combination of axes, as long as the destination is safe.',p:'Moves one rank forward. Captures forward plus one step along X, Z, or W. Promotes on the last rank.'}; | |
| let board=E.initial(),turn='w',mode='ai',history=[],halfmove=0,keys=[E.key(board,turn)],selected=null,selectedMoves=[],result=null,thinking=false,aiTimer=null,gameVersion=0,hintTarget=null; | |
| let spatialView=null,spatialTarget=null; | |
| let threatInspection=null,threatHold=null,lastThreatHold=null; | |
| let moveAnimation=null,moveFrame=0; | |
| const moveMotionPreference=matchMedia('(prefers-reduced-motion: reduce)'); | |
| const cells=Array(64),storageKey='fourfold-game-v1'; | |
| function pieceSVG(p){return ``;} | |
| function buildBoards(){for(const w of [1,0]){const axis=document.createElement('div');axis.className='axis-w';axis.textContent=`W = ${w}`;$('slice-grid').append(axis);for(const z of [0,1]){const section=document.createElement('div');section.className='slice';const title=`${w?'Upper':'Lower'} ${z?'far':'near'}`;section.innerHTML=`4321abcd`;const grid=section.querySelector('.board');for(let y=3;y>=0;y--)for(let x=0;x<4;x++){const i=E.index([x,y,z,w]),button=document.createElement('button');button.className='cell';button.type='button';button.dataset.index=i;button.addEventListener('click',()=>onSquare(i));button.addEventListener('mouseenter',()=>preview(i));button.addEventListener('focus',()=>preview(i));button.addEventListener('mouseleave',()=>inspect());button.addEventListener('keydown',ev=>navigateBoard(ev,i));bindThreatInspection(button,()=>i);cells[i]=button;grid.append(button);}$('slice-grid').append(section);}}} | |
| function navigateBoard(ev,i){const c=E.coords[i].slice();const axis=ev.altKey?2:0;if(ev.key==='ArrowLeft')c[axis]--;else if(ev.key==='ArrowRight')c[axis]++;else if(ev.key==='ArrowUp')c[axis+1]++;else if(ev.key==='ArrowDown')c[axis+1]--;else return;ev.preventDefault();if(c.every((v,k)=>v>=0&&v<E.dims[k]))cells[E.index(c)].focus();} | |
| function save(){try{localStorage.setItem(storageKey,JSON.stringify({version:1,mode,history:history.map(h=>({from:h.from,to:h.to}))}));}catch{}} | |
| function restore(){try{const data=JSON.parse(localStorage.getItem(storageKey));if(!data||data.version!==1||!['ai','local'].includes(data.mode)||!Array.isArray(data.history)||data.history.length>2000)return;let b=E.initial(),c='w',hm=0,hs=[],ks=[E.key(b,c)];for(const m of data.history){if(!Number.isInteger(m.from)||!Number.isInteger(m.to)||!E.legal(b,c,m.from).some(v=>v.to===m.to)||E.outcome(b,c,hm,ks))return;const entry={...m,board:b,turn:c,halfmove:hm,piece:b[m.from],captured:b[m.to]};hs.push(entry);hm=entry.piece.t==='p'||entry.captured?0:hm+1;b=E.apply(b,m);c=E.other(c);ks.push(E.key(b,c));}board=b;turn=c;halfmove=hm;history=hs;keys=ks;mode=data.mode;}catch{}} | |
| function render(){result=E.outcome(board,turn,halfmove,keys);const last=history.at(-1),check=!moveAnimation&&E.inCheck(threatInspection?.position||board,turn);for(let i=0;i<64;i++){const p=visiblePiece(i),[x,y]=E.coords[i],legal=!threatInspection&&selectedMoves.some(m=>m.to===i);let cl='cell'+((x+y)%2===0?' dark':'');if(last&&(last.from===i||last.to===i))cl+=' last';if(selected===i)cl+=' selected';if(legal)cl+=' legal'+(p?' capture':'');if(p?.t==='k'&&p.c===turn&&check)cl+=' checked';if(hintTarget===i)cl+=' hint';if(threatInspection?.target===i)cl+=' threat-target';if(threatInspection?.attackers.includes(i))cl+=' threat-attacker';cells[i].className=cl;cells[i].innerHTML=p?pieceSVG(p):'';if(threatInspection?.preview&&threatInspection.target===i)cells[i].querySelector('.piece')?.classList.add('threat-ghost');cells[i].setAttribute('aria-label',`${E.address(i)}, ${p?(p.c==='w'?'White ':'Black ')+E.names[p.t]:'empty'}${legal?', legal '+(p?'capture':'destination'):''}`);cells[i].setAttribute('aria-pressed',selected===i?'true':'false');cells[i].title=cells[i].getAttribute('aria-label');} | |
| $('mode').value=mode;$('undo-button').disabled=!history.length;$('demo-button').disabled=!!result||thinking||!!moveAnimation||(mode==='ai'&&turn==='b');$('turn-token').className='turn-token'+((moveAnimation?.entry.piece.c??turn)==='b'?' black':''); | |
| if(moveAnimation){const entry=moveAnimation.entry;$('status-title').textContent=(entry.piece.c==='w'?'White ':'Black ')+E.names[entry.piece.t].toLowerCase()+' moving…';$('status-text').textContent=moveDescription(entry);} | |
| else if(result){$('status-title').textContent=result.type==='mate'?`${result.winner==='w'?'White':'Black'} wins`:'Game drawn';$('status-text').textContent=({mate:'Checkmate.',stalemate:'Stalemate. No legal moves.',bare:'Only the two kings remain.',fifty:'100 turns without a pawn move or capture.',repetition:'Third repetition of this position.'})[result.type];} | |
| else{$('status-title').textContent=thinking?'Black is thinking…':`${turn==='w'?'White':'Black'}${check?' in check':' to move'}`;$('status-text').textContent=check?'Your king must escape check.':'Right-click or long-press to reveal attackers.';} | |
| $('status-text').hidden=!$('status-text').textContent; | |
| $('slice-grid').setAttribute('aria-busy',String(!!moveAnimation));$('space-svg').setAttribute('aria-busy',String(!!moveAnimation)); | |
| inspect();renderHistory();renderThreatSummary();spatialView?.draw();updateFlatMotion();updateFlatThreats();} | |
| function inspect(target=null){if(threatInspection){$('inspector').hidden=true;return;}spatialTarget=target;spatialView?.draw();$('inspector').hidden=selected===null;if(selected===null)return;const p=board[selected];$('piece-title').textContent=`${p.c==='w'?'White':'Black'} ${E.names[p.t].toLowerCase()}`;$('move-count').textContent=`${selectedMoves.length} legal ${selectedMoves.length===1?'move':'moves'}`;const from=E.coords[selected],to=target===null?from:E.coords[target];const changed=from.map((v,i)=>v!==to[i]?'XYZW'[i]:null).filter(Boolean);$('piece-description').textContent=target===null?descriptions[p.t]:`${E.address(selected)} → ${E.address(target)}. ${changed.length===1?'Only '+changed[0]+' changes':changed.join(' + ')+' change together'}.${board[target]?' Captures '+E.names[board[target].t].toLowerCase()+'.':''}`;$('coordinates').innerHTML=from.map((v,i)=>`${'XYZW'[i]} ${i===0?'abcd'[v]:i===1?v+1:v}${v!==to[i]?' → '+(i===0?'abcd'[to[i]]:i===1?to[i]+1:to[i]):''}`).join('');} | |
| function preview(i){if(selected!==null&&selectedMoves.some(m=>m.to===i))inspect(i);else inspect();} | |
| function renderHistory(){$('ply-count').textContent=`${history.length} ${history.length===1?'move':'moves'}`;$('move-list').innerHTML=history.length?history.map((h,i)=>`${i+1}.${h.piece.c==='w'?'White':'Black'} ${E.names[h.piece.t].toLowerCase()}${h.captured?' × '+E.names[h.captured.t].toLowerCase():''}${h.piece.t==='p'&&E.coords[h.to][1]===(h.piece.c==='w'?3:0)?' → queen':''}${E.address(h.from)} → ${E.address(h.to)}`).reverse().join(''):' No moves yet. ';const captures=history.filter(h=>h.captured);$('captured').innerHTML=captures.length?['w','b'].map(c=>{const taken=captures.filter(h=>h.piece.c===c).map(h=>E.names[h.captured.t].toLowerCase());return taken.length?`${c==='w'?'White':'Black'} captured ${taken.join(', ')}`:'';}).join(''):'';} | |
| function onSquare(i){if(result||thinking||moveAnimation||(mode==='ai'&&turn==='b'))return;clearThreatState();const move=selectedMoves.find(m=>m.to===i);if(move){makeMove(move);return;}hintTarget=null;if(selected===i){selected=null;selectedMoves=[];}else if(board[i]?.c===turn){selected=i;selectedMoves=E.legal(board,turn,i);}else{selected=null;selectedMoves=[];}render();} | |
| function makeMove(m){if(moveAnimation||!E.legal(board,turn,m.from).some(v=>v.to===m.to))return;clearThreatState();const entry={...m,board,turn,halfmove,piece:board[m.from],captured:board[m.to]};history.push(entry);halfmove=entry.piece.t==='p'||entry.captured?0:halfmove+1;board=E.apply(board,m);turn=E.other(turn);keys.push(E.key(board,turn));selected=null;selectedMoves=[];hintTarget=null;spatialTarget=null;save();animateMove(entry);} | |
| function scheduleComputer(){if(mode!=='ai'||turn!=='b'||result||thinking||moveAnimation)return;thinking=true;render();const version=gameVersion;aiTimer=setTimeout(()=>{aiTimer=null;if(version!==gameVersion)return;const move=E.computer(board,turn);thinking=false;if(move)makeMove(move);else render();},350);} | |
| function cancelComputer(){gameVersion++;clearTimeout(aiTimer);aiTimer=null;thinking=false;cancelMoveAnimation();clearThreatState();} | |
| function undo(){if(!history.length)return;cancelComputer();let count=mode==='ai'&&turn==='w'&&history.length>=2?2:1;while(count--&&history.length){const h=history.pop();board=h.board;turn=h.turn;halfmove=h.halfmove;keys.pop();}selected=null;selectedMoves=[];hintTarget=null;save();render();scheduleComputer();} | |
| function newGame(){cancelComputer();board=E.initial();turn='w';history=[];halfmove=0;keys=[E.key(board,turn)];selected=null;selectedMoves=[];hintTarget=null;save();render();} | |
| // Both renderers use the same move and eased progress. The rules position is | |
| // committed once; the pre-move position remains visible until the piece lands. | |
| function visiblePiece(i){ | |
| if(!moveAnimation)return (threatInspection?.position||board)[i]; | |
| return i===moveAnimation.entry.from?null:moveAnimation.entry.board[i]; | |
| } | |
| function moveCoordinates(progress){ | |
| const {from,to}=moveAnimation.entry,a=E.coords[from],b=E.coords[to]; | |
| return a.map((v,i)=>v+(b[i]-v)*progress); | |
| } | |
| function moveDescription(entry){ | |
| const axes=E.coords[entry.from].map((v,i)=>v!==E.coords[entry.to][i]?'XYZW'[i]:null).filter(Boolean); | |
| return E.address(entry.from)+' → '+E.address(entry.to)+' · '+axes.join(' + '); | |
| } | |
| function updateFlatMotion(){ | |
| const overlay=$('board-motion'); | |
| if(!moveAnimation){overlay.replaceChildren();return;} | |
| const {entry,progress}=moveAnimation,root=$('slice-grid').getBoundingClientRect(); | |
| const from=cells[entry.from].getBoundingClientRect(),to=cells[entry.to].getBoundingClientRect(); | |
| const a={x:from.x-root.x+from.width/2,y:from.y-root.y+from.height/2}; | |
| const b={x:to.x-root.x+to.width/2,y:to.y-root.y+to.height/2}; | |
| const size=Math.min(72,from.width*.76),lift=entry.piece.t==='n'?from.height*.52:0; | |
| const source=E.coords[entry.from],destination=E.coords[entry.to]; | |
| const crossesZ=source[2]!==destination[2],crossesW=source[3]!==destination[3]; | |
| const plane=cells[entry.from].closest('.board').getBoundingClientRect(); | |
| let bendX=0,bendY=0; | |
| // Bow cross-board travel toward a gutter so it reads as a change of board. | |
| if(crossesW&&!crossesZ){ | |
| const lane=source[0]<1.5?Math.max(4,plane.left-root.left-14):Math.min(root.width-4,plane.right-root.left+14); | |
| bendX=lane-a.x; | |
| }else if(crossesZ&&!crossesW){ | |
| const lane=source[1]<1.5?Math.min(root.height-4,plane.bottom-root.top+14):Math.max(4,plane.top-root.top-14); | |
| bendY=lane-a.y; | |
| }else if(crossesZ&&crossesW){bendX=(source[2]===0?-1:1)*from.width*.65;} | |
| const position=t=>({x:a.x+(b.x-a.x)*t+Math.sin(Math.PI*t)*bendX,y:a.y+(b.y-a.y)*t+Math.sin(Math.PI*t)*(bendY-lift)}); | |
| overlay.setAttribute('viewBox','0 0 '+root.width+' '+root.height); | |
| if(!overlay.firstElementChild){ | |
| overlay.innerHTML=''; | |
| } | |
| const samples=[]; | |
| for(let n=0;n<=20;n++){const p=position(n/20);samples.push((n?'L':'M')+p.x+','+p.y);} | |
| overlay.querySelector('path').setAttribute('d',samples.join(' ')); | |
| const token=overlay.querySelector('.moving-piece'),p=position(progress); | |
| token.setAttribute('x',p.x-size/2);token.setAttribute('y',p.y-size/2);token.setAttribute('width',size);token.setAttribute('height',size); | |
| const captured=cells[entry.to].querySelector('.piece'); | |
| if(captured)captured.style.opacity=String(1-Math.max(0,(progress-.82)/.18)); | |
| } | |
| function updateMoveFrame(){updateFlatMotion();spatialView?.updateMotion();} | |
| function cancelMoveAnimation(){ | |
| cancelAnimationFrame(moveFrame);moveFrame=0;moveAnimation=null; | |
| $('board-motion').replaceChildren(); | |
| } | |
| function finishMoveAnimation(){ | |
| if(!moveAnimation)return; | |
| cancelMoveAnimation();render();scheduleComputer(); | |
| } | |
| function animateMove(entry){ | |
| if(moveMotionPreference.matches||document.hidden){render();scheduleComputer();return;} | |
| const crossBoard=E.coords[entry.from][2]!==E.coords[entry.to][2]||E.coords[entry.from][3]!==E.coords[entry.to][3]; | |
| const motion={entry,progress:0,started:null,duration:crossBoard?1100:entry.piece.t==='n'?850:650}; | |
| moveAnimation=motion;render(); | |
| function tick(now){ | |
| if(moveAnimation!==motion)return; | |
| if(motion.started===null)motion.started=now; | |
| const linear=Math.max(0,Math.min(1,(now-motion.started)/motion.duration)); | |
| motion.progress=linear*linear*(3-2*linear); | |
| updateMoveFrame(); | |
| if(linear>=1){finishMoveAnimation();return;} | |
| moveFrame=requestAnimationFrame(tick); | |
| } | |
| moveFrame=requestAnimationFrame(tick); | |
| } | |
| window.addEventListener('resize',()=>{if(moveAnimation)updateMoveFrame();}); | |
| document.addEventListener('visibilitychange',()=>{if(document.hidden)finishMoveAnimation();}); | |
| moveMotionPreference.addEventListener('change',event=>{if(event.matches)finishMoveAnimation();}); | |
| // Threat inspection reads a separate position and never changes the game state. | |
| function analyzeThreats(position,activeColor,target,source=null){ | |
| const moving=source===null?null:position[source]; | |
| const preview=!!moving&&source!==target&&position[target]?.t!=='k'&&E.canReach(position,source,target); | |
| const legalMove=!preview||!E.inCheck(E.apply(position,{from:source,to:target}),moving.c); | |
| const viewed=preview?E.apply(position,{from:source,to:target}):position; | |
| const color=preview?moving.c:position[target]?.c||moving?.c||activeColor; | |
| const probe=viewed.slice(); | |
| // An empty square is tested as occupied by this side. The placeholder is | |
| // removed by each simulated capture, so it adds no defensive attacks. | |
| if(!probe[target])probe[target]={t:'p',c:color}; | |
| const kingSquare=probe[target].t==='k',attackers=[]; | |
| for(let from=0;from<64;from++){ | |
| const piece=probe[from]; | |
| if(!piece||piece.c===color||!E.canReach(probe,from,target,true))continue; | |
| // Kings must respect all controlled squares, including pinned defenders. | |
| // For other targets, only captures that keep the attacker's king safe count. | |
| if(kingSquare||!E.inCheck(E.apply(probe,{from,to:target}),piece.c))attackers.push(from); | |
| } | |
| return {target,color,preview,legalMove,from:preview?source:null,position:viewed,attackers,moving,kingSquare}; | |
| } | |
| function clearThreatState(){cancelThreatHold();threatInspection=null;} | |
| function toggleThreats(index){ | |
| if(!Number.isInteger(index)||index<0||index>=64||moveAnimation)return; | |
| if(threatInspection?.target===index){threatInspection=null;render();return;} | |
| threatInspection=analyzeThreats(board,mode==='ai'?'w':turn,index,selected); | |
| spatialTarget=null;hintTarget=null;render(); | |
| } | |
| function threatContext(){ | |
| if(!threatInspection)return ''; | |
| const t=threatInspection; | |
| if(t.preview)return 'After '+(t.moving.c==='w'?'White ':'Black ')+E.names[t.moving.t].toLowerCase()+' '+E.address(t.from)+' → '+E.address(t.target)+(t.legalMove?'':' · Leaves your king in check'); | |
| return 'Current position · '+(t.color==='w'?'White':'Black')+' square'; | |
| } | |
| function renderThreatSummary(){ | |
| $('threat-summary').hidden=!threatInspection; | |
| if(!threatInspection)return; | |
| const t=threatInspection,n=t.attackers.length; | |
| $('threat-title').textContent=E.address(t.target)+' · '+n+' '+(n===1?'attacker':'attackers'); | |
| $('threat-context').textContent=threatContext();$('threat-context').classList.toggle('threat-warning',t.preview&&!t.legalMove); | |
| $('threat-list').textContent=n?t.attackers.map(i=>{const p=t.position[i];return (p.c==='w'?'White ':'Black ')+E.names[p.t].toLowerCase()+' at '+E.address(i);}).join(' · '):'No immediate enemy attackers in this position.'; | |
| } | |
| function attackMarker(id){return '';} | |
| function attackArrow(a,b,id,padding=15){ | |
| const dx=b.x-a.x,dy=b.y-a.y,length=Math.hypot(dx,dy); | |
| if(length<2)return ''; | |
| const inset=Math.min(padding,length*.25),start=Math.min(padding*.6,length*.2); | |
| const x1=a.x+dx/length*start,y1=a.y+dy/length*start,x2=b.x-dx/length*inset,y2=b.y-dy/length*inset; | |
| const path='M'+x1+','+y1+' L'+x2+','+y2; | |
| return ''; | |
| } | |
| function updateFlatThreats(){ | |
| const svg=$('board-threats'); | |
| if(!threatInspection){svg.replaceChildren();return;} | |
| const t=threatInspection,root=$('slice-grid').getBoundingClientRect(); | |
| const center=i=>{const r=cells[i].getBoundingClientRect();return {x:r.x-root.x+r.width/2,y:r.y-root.y+r.height/2};}; | |
| svg.setAttribute('viewBox','0 0 '+root.width+' '+root.height); | |
| const padding=cells[t.target].getBoundingClientRect().width*.3; | |
| svg.innerHTML=attackMarker('attack-arrow-flat')+t.attackers.map(i=>attackArrow(center(i),center(t.target),'attack-arrow-flat',padding)).join(''); | |
| } | |
| function cancelThreatHold(){if(threatHold)clearTimeout(threatHold.timer);threatHold=null;} | |
| function bindThreatInspection(element,indexAt,onHold=()=>{},movementLimit=8){ | |
| function fire(press){ | |
| if(threatHold!==press||press.fired||press.position!==board)return; | |
| clearTimeout(press.timer);press.fired=true; | |
| lastThreatHold={root:element,until:performance.now()+1200};onHold();toggleThreats(press.index); | |
| } | |
| element.addEventListener('contextmenu',event=>{ | |
| const index=indexAt(event); | |
| if(index===null||index===undefined)return; | |
| event.preventDefault();event.stopPropagation(); | |
| if(lastThreatHold?.root===element&&performance.now()<lastThreatHold.until)return; | |
| if(threatHold?.root===element){fire(threatHold);return;} | |
| toggleThreats(index); | |
| }); | |
| element.addEventListener('keydown',event=>{ | |
| if(event.key!=='ContextMenu'&&!(event.key==='F10'&&event.shiftKey))return; | |
| const index=indexAt(event);if(index===null||index===undefined)return; | |
| event.preventDefault();event.stopPropagation();toggleThreats(index); | |
| }); | |
| element.addEventListener('pointerdown',event=>{ | |
| lastThreatHold=null;cancelThreatHold(); | |
| if(!['touch','pen'].includes(event.pointerType)||event.button!==0||event.isPrimary===false)return; | |
| const index=indexAt(event);if(index===null||index===undefined)return; | |
| const press={root:element,id:event.pointerId,index,x:event.clientX,y:event.clientY,position:board,fired:false,timer:null}; | |
| threatHold=press;press.timer=setTimeout(()=>fire(press),550); | |
| },{passive:true}); | |
| element.addEventListener('pointermove',event=>{ | |
| const press=threatHold;if(!press||press.id!==event.pointerId||press.root!==element)return; | |
| if(Math.hypot(event.clientX-press.x,event.clientY-press.y)>movementLimit)cancelThreatHold(); | |
| },{passive:true}); | |
| for(const name of ['pointerup','pointercancel','lostpointercapture'])element.addEventListener(name,event=>{ | |
| if(threatHold?.id!==event.pointerId||threatHold.root!==element)return; | |
| if(threatHold.fired)lastThreatHold={root:element,until:performance.now()+1200};cancelThreatHold(); | |
| },{passive:true}); | |
| // Suppress the synthetic tap after a long-press, while allowing the next | |
| // deliberate tap: a new pointerdown clears this suppression. | |
| element.addEventListener('click',event=>{ | |
| if(lastThreatHold?.root===element&&performance.now()<lastThreatHold.until){event.preventDefault();event.stopImmediatePropagation();} | |
| },true); | |
| } | |
| window.addEventListener('blur',cancelThreatHold); | |
| window.addEventListener('resize',updateFlatThreats); | |
| document.addEventListener('visibilitychange',()=>{if(document.hidden)cancelThreatHold();}); | |
| buildBoards();restore();render();scheduleComputer(); | |
| $('clear-threats').addEventListener('click',()=>{const target=threatInspection?.target;threatInspection=null;render();if(target!==undefined)cells[target].focus({preventScroll:true});}); | |
| $('rules-button').addEventListener('click',()=>$('rules-dialog').showModal()); | |
| document.querySelectorAll('[data-close]').forEach(b=>b.addEventListener('click',()=>$(b.dataset.close |