]> git.za3k.com Git - za3k.git/commitdiff
Distortion Chess
authorZachary Vance <za3k@za3k.com>
Wed, 4 Mar 2026 04:14:13 +0000 (23:14 -0500)
committerZachary Vance <za3k@za3k.com>
Wed, 4 Mar 2026 04:14:13 +0000 (23:14 -0500)
distortion-chess/index.html [new file with mode: 0644]
software.md

diff --git a/distortion-chess/index.html b/distortion-chess/index.html
new file mode 100644 (file)
index 0000000..dc2f0fb
--- /dev/null
@@ -0,0 +1,2355 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Distortion Chess</title>
+  <style>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+  min-height: 100vh;
+  color: #e0e0e0;
+}
+
+#app {
+  max-width: 1400px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 10px 20px;
+  background: rgba(0, 0, 0, 0.3);
+  border-radius: 10px;
+}
+
+h1 {
+  font-size: 2em;
+  background: linear-gradient(90deg, #8b5cf6, #ec4899);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+#game-info {
+  display: flex;
+  gap: 20px;
+  font-size: 1.1em;
+}
+
+#turn-indicator {
+  padding: 8px 16px;
+  border-radius: 20px;
+  font-weight: bold;
+}
+
+#turn-indicator.white {
+  background: #f0f0f0;
+  color: #333;
+}
+
+#turn-indicator.black {
+  background: #333;
+  color: #f0f0f0;
+}
+
+#turn-indicator.my-turn {
+  animation: my-turn-pulse 0.8s ease-in-out infinite alternate;
+  font-size: 1.1em;
+  text-transform: uppercase;
+  letter-spacing: 1px;
+}
+
+#turn-indicator.white.my-turn {
+  background: linear-gradient(135deg, #f0f0f0, #e2e8f0);
+  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
+}
+
+#turn-indicator.black.my-turn {
+  background: linear-gradient(135deg, #333, #1e293b);
+  box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
+}
+
+@keyframes my-turn-pulse {
+  from { transform: scale(1); }
+  to { transform: scale(1.05); }
+}
+
+#distortion-counter {
+  padding: 8px 16px;
+  background: rgba(139, 92, 246, 0.3);
+  border-radius: 20px;
+  border: 1px solid #8b5cf6;
+}
+
+main {
+  display: flex;
+  gap: 30px;
+  align-items: flex-start;
+}
+
+#board-container {
+  flex: 0 0 auto;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.captured-pieces {
+  min-height: 36px;
+  padding: 4px 8px;
+  background: rgba(0, 0, 0, 0.3);
+  border-radius: 8px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 2px;
+  align-items: center;
+}
+
+.captured-pieces .piece {
+  font-size: 24px;
+}
+
+.captured-pieces .piece.white {
+  color: #f8fafc;
+  text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
+}
+
+.captured-pieces .piece.black {
+  color: #1e293b;
+  filter: drop-shadow(0 0 3px rgba(255,255,255,0.7)) drop-shadow(0 0 1px rgba(255,255,255,0.7));
+}
+
+#board {
+  display: grid;
+  grid-template-columns: repeat(11, 50px);
+  grid-template-rows: repeat(11, 50px);
+  gap: 1px;
+  background: #0a0a1a;
+  padding: 2px;
+  border-radius: 8px;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
+}
+
+.square {
+  width: 50px;
+  height: 50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 32px;
+  cursor: pointer;
+  transition: all 0.2s;
+  position: relative;
+}
+
+.square.void {
+  background: #0a0a1a;
+  cursor: default;
+}
+
+.square.tile {
+  background: #2d3748;
+}
+
+.square.tile.light {
+  background: #4a5568;
+}
+
+.square.tile.dark {
+  background: #2d3748;
+}
+
+.square.tile-border-top {
+  border-top: 3px solid #8b5cf6;
+}
+
+.square.tile-border-bottom {
+  border-bottom: 3px solid #8b5cf6;
+}
+
+.square.tile-border-left {
+  border-left: 3px solid #8b5cf6;
+}
+
+.square.tile-border-right {
+  border-right: 3px solid #8b5cf6;
+}
+
+.square.voided-tile {
+  background: repeating-linear-gradient(
+    45deg,
+    #1a1a2e,
+    #1a1a2e 10px,
+    #0a0a1a 10px,
+    #0a0a1a 20px
+  );
+}
+
+.square.selected {
+  background: rgba(236, 72, 153, 0.5) !important;
+  box-shadow: inset 0 0 10px rgba(236, 72, 153, 0.8);
+}
+
+.square.valid-move {
+  background: rgba(34, 197, 94, 0.3) !important;
+}
+
+.square.valid-move::after {
+  content: '';
+  width: 16px;
+  height: 16px;
+  background: rgba(34, 197, 94, 0.6);
+  border-radius: 50%;
+  position: absolute;
+}
+
+.square.valid-capture {
+  background: rgba(239, 68, 68, 0.3) !important;
+  box-shadow: inset 0 0 0 3px rgba(239, 68, 68, 0.6);
+}
+
+.square.last-move {
+  box-shadow: inset 0 0 0 3px rgba(250, 204, 21, 0.5);
+}
+
+.square.boulder {
+  background: #6b7280 !important;
+}
+
+.square.boulder::before {
+  content: '';
+  width: 36px;
+  height: 36px;
+  background: linear-gradient(145deg, #9ca3af, #4b5563);
+  border-radius: 50%;
+  box-shadow: inset 2px 2px 4px rgba(255,255,255,0.2),
+              inset -2px -2px 4px rgba(0,0,0,0.3);
+}
+
+.square.check {
+  animation: check-pulse 0.5s ease-in-out infinite alternate;
+}
+
+@keyframes check-pulse {
+  from { box-shadow: inset 0 0 10px rgba(239, 68, 68, 0.5); }
+  to { box-shadow: inset 0 0 20px rgba(239, 68, 68, 0.9); }
+}
+
+.piece {
+  font-size: 36px;
+  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
+  user-select: none;
+  z-index: 1;
+}
+
+.piece.white {
+  color: #f8fafc;
+  filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.8));
+}
+
+.piece.black {
+  color: #1e293b;
+  filter: drop-shadow(1px 1px 2px rgba(255,255,255,0.3));
+}
+
+.square.tile-selected {
+  background: rgba(139, 92, 246, 0.5) !important;
+  box-shadow: inset 0 0 15px rgba(139, 92, 246, 0.8);
+}
+
+#sidebar {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+#sidebar > div {
+  background: rgba(0, 0, 0, 0.3);
+  padding: 20px;
+  border-radius: 10px;
+}
+
+#sidebar h2 {
+  margin-bottom: 15px;
+  color: #8b5cf6;
+  font-size: 1.2em;
+}
+
+#sidebar h3 {
+  margin-bottom: 10px;
+  color: #a78bfa;
+  font-size: 1em;
+}
+
+#player-color {
+  font-size: 1.2em;
+  margin-bottom: 10px;
+}
+
+#game-status {
+  padding: 10px;
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 5px;
+  margin-bottom: 10px;
+}
+
+#check-indicator {
+  color: #ef4444;
+  font-weight: bold;
+  font-size: 1.1em;
+}
+
+#distortion-panel {
+  border: 2px solid #8b5cf6;
+  animation: glow 1s ease-in-out infinite alternate;
+}
+
+@keyframes glow {
+  from { box-shadow: 0 0 10px rgba(139, 92, 246, 0.3); }
+  to { box-shadow: 0 0 20px rgba(139, 92, 246, 0.6); }
+}
+
+#distortion-card {
+  font-size: 1.5em;
+  text-align: center;
+  padding: 15px;
+  background: linear-gradient(135deg, #4c1d95, #7c3aed);
+  border-radius: 8px;
+  margin-bottom: 15px;
+  text-transform: uppercase;
+  font-weight: bold;
+}
+
+#distortion-instructions {
+  margin-bottom: 15px;
+  line-height: 1.5;
+}
+
+#distortion-controls {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+#distortion-controls button {
+  padding: 8px 16px;
+  background: #7c3aed;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 0.9em;
+  transition: all 0.2s;
+}
+
+#distortion-controls button:hover {
+  background: #8b5cf6;
+  transform: translateY(-2px);
+}
+
+#distortion-controls button:disabled {
+  background: #4a5568;
+  cursor: not-allowed;
+  transform: none;
+}
+
+#promotion-dialog {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: #1a1a2e;
+  padding: 30px;
+  border-radius: 15px;
+  border: 2px solid #8b5cf6;
+  z-index: 100;
+}
+
+#promotion-options {
+  display: flex;
+  gap: 15px;
+}
+
+#promotion-options button {
+  width: 60px;
+  height: 60px;
+  font-size: 40px;
+  background: #2d3748;
+  border: 2px solid #4a5568;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+#promotion-options button:hover {
+  background: #4a5568;
+  border-color: #8b5cf6;
+  transform: scale(1.1);
+}
+
+#invite-url {
+  width: 100%;
+  padding: 10px;
+  background: #1a1a2e;
+  border: 1px solid #4a5568;
+  border-radius: 5px;
+  color: #e0e0e0;
+  font-size: 0.9em;
+  margin-bottom: 10px;
+}
+
+#copy-url {
+  width: 100%;
+  padding: 10px;
+  background: #22c55e;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-weight: bold;
+  transition: all 0.2s;
+}
+
+#copy-url:hover {
+  background: #16a34a;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 8px;
+}
+
+.boulder-icon {
+  width: 24px;
+  height: 24px;
+  background: linear-gradient(145deg, #9ca3af, #4b5563);
+  border-radius: 50%;
+}
+
+.void-icon {
+  width: 24px;
+  height: 24px;
+  background: repeating-linear-gradient(
+    45deg,
+    #1a1a2e,
+    #1a1a2e 4px,
+    #0a0a1a 4px,
+    #0a0a1a 8px
+  );
+  border: 1px solid #4a5568;
+}
+
+.hidden {
+  display: none !important;
+}
+
+.game-over-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.8);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 200;
+}
+
+.game-over-content {
+  background: linear-gradient(135deg, #1a1a2e, #16213e);
+  padding: 40px 60px;
+  border-radius: 20px;
+  text-align: center;
+  border: 3px solid #8b5cf6;
+  box-shadow: 0 0 50px rgba(139, 92, 246, 0.5);
+}
+
+.game-over-content h2 {
+  font-size: 2.5em;
+  margin-bottom: 20px;
+  background: linear-gradient(90deg, #8b5cf6, #ec4899);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.game-over-content p {
+  font-size: 1.3em;
+  margin-bottom: 30px;
+}
+
+.game-over-content button {
+  padding: 15px 40px;
+  font-size: 1.1em;
+  background: #7c3aed;
+  color: white;
+  border: none;
+  border-radius: 10px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.game-over-content button:hover {
+  background: #8b5cf6;
+  transform: translateY(-3px);
+}
+
+.piece.ghost {
+  opacity: 0.6;
+  filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.8));
+}
+
+.square.ghost-tile {
+  background: rgba(139, 92, 246, 0.3) !important;
+  box-shadow: inset 0 0 10px rgba(139, 92, 246, 0.5);
+}
+
+.square.ghost-boulder::before {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 60%;
+  height: 60%;
+  background: linear-gradient(145deg, rgba(156, 163, 175, 0.5), rgba(75, 85, 99, 0.5));
+  border-radius: 50%;
+  z-index: 1;
+}
+
+.square.will-void {
+  background: rgba(220, 38, 38, 0.3) !important;
+  box-shadow: inset 0 0 15px rgba(220, 38, 38, 0.6);
+}
+
+.direction-group {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  width: 100%;
+  margin-bottom: 5px;
+}
+
+.direction-group span {
+  width: 60px;
+  font-weight: bold;
+}
+
+.dist-btn {
+  width: 40px !important;
+  padding: 6px 8px !important;
+}
+
+.submit-btn {
+  width: 100%;
+  margin-top: 10px;
+  background: #22c55e !important;
+}
+
+.submit-btn:hover {
+  background: #16a34a !important;
+}
+
+#distortion-controls button.disabled {
+  opacity: 0.4;
+}
+
+#chat-container {
+  position: fixed;
+  bottom: 20px;
+  right: 20px;
+  width: 300px;
+  background: rgba(26, 26, 46, 0.95);
+  border: 1px solid #4a5568;
+  border-radius: 10px;
+  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4);
+  z-index: 100;
+  display: flex;
+  flex-direction: column;
+}
+
+#chat-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 15px;
+  background: rgba(139, 92, 246, 0.3);
+  border-radius: 10px 10px 0 0;
+  cursor: pointer;
+}
+
+#chat-header span {
+  font-weight: bold;
+  color: #e0e0e0;
+}
+
+#chat-toggle {
+  background: none;
+  border: none;
+  color: #e0e0e0;
+  font-size: 1.2em;
+  cursor: pointer;
+  width: 24px;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+#chat-body {
+  display: flex;
+  flex-direction: column;
+  height: 300px;
+}
+
+#chat-container.minimized #chat-body {
+  display: none;
+}
+
+#chat-container.minimized #chat-header {
+  border-radius: 10px;
+}
+
+#chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.chat-message {
+  padding: 6px 10px;
+  border-radius: 8px;
+  max-width: 85%;
+  word-wrap: break-word;
+}
+
+.chat-message.mine {
+  background: rgba(139, 92, 246, 0.4);
+  align-self: flex-end;
+}
+
+.chat-message.theirs {
+  background: rgba(74, 85, 104, 0.6);
+  align-self: flex-start;
+}
+
+.chat-message .sender {
+  font-size: 0.75em;
+  color: #a0aec0;
+  margin-bottom: 2px;
+}
+
+.chat-message .text {
+  color: #e0e0e0;
+}
+
+#chat-input-area {
+  display: flex;
+  gap: 8px;
+  padding: 10px;
+  border-top: 1px solid #4a5568;
+}
+
+#chat-input {
+  flex: 1;
+  padding: 8px 12px;
+  background: #1a1a2e;
+  border: 1px solid #4a5568;
+  border-radius: 5px;
+  color: #e0e0e0;
+  font-size: 0.9em;
+}
+
+#chat-input:focus {
+  outline: none;
+  border-color: #8b5cf6;
+}
+
+#chat-send {
+  padding: 8px 16px;
+  background: #7c3aed;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 0.9em;
+}
+
+#chat-send:hover {
+  background: #8b5cf6;
+}
+
+#chat-container.new-message #chat-header {
+  animation: chat-flash 0.5s ease-in-out 4;
+}
+
+@keyframes chat-flash {
+  0%, 100% { background: rgba(139, 92, 246, 0.3); }
+  50% { background: rgba(236, 72, 153, 0.6); }
+}
+  </style>
+</head>
+<body>
+  <div id="app">
+    <header>
+      <div class="nav"><a href="/">za3k</a> &gt; <a href="/software">software</a> &gt; distortion chess</div>
+      <h1>Distortion Chess</h1>
+      <div id="game-info">
+        <span id="turn-indicator"></span>
+        <span id="distortion-counter"></span>
+      </div>
+    </header>
+    
+    <main>
+      <div id="board-container">
+        <div id="captured-black" class="captured-pieces"></div>
+        <div id="board"></div>
+        <div id="captured-white" class="captured-pieces"></div>
+      </div>
+      
+      <aside id="sidebar">
+        <div id="status-panel">
+          <h2>Game Status</h2>
+          <div id="player-color"></div>
+          <div id="game-status"></div>
+          <div id="check-indicator"></div>
+        </div>
+        
+        <div id="distortion-panel" class="hidden">
+          <h2>Distortion Event</h2>
+          <div id="distortion-card"></div>
+          <div id="distortion-instructions"></div>
+          <div id="distortion-controls"></div>
+        </div>
+        
+        <div id="promotion-dialog" class="hidden">
+          <h2>Promote Pawn</h2>
+          <div id="promotion-options"></div>
+        </div>
+        
+        <div id="invite-panel">
+          <h2>Invite Opponent</h2>
+          <input type="text" id="invite-url" readonly>
+          <button id="copy-url">Copy Link</button>
+        </div>
+        
+        <div id="legend">
+          <h3>Legend</h3>
+          <div class="legend-item"><span class="boulder-icon"></span> Boulder</div>
+          <div class="legend-item"><span class="void-icon"></span> Void</div>
+        </div>
+      </aside>
+    </main>
+  </div>
+  
+  <div id="chat-container" class="minimized">
+    <div id="chat-header">
+      <span>Chat</span>
+      <button id="chat-toggle">+</button>
+    </div>
+    <div id="chat-body">
+      <div id="chat-messages"></div>
+      <div id="chat-input-area">
+        <input type="text" id="chat-input" placeholder="Type a message...">
+        <button id="chat-send">Send</button>
+      </div>
+    </div>
+  </div>
+  
+  <script>
+// Piece unicode symbols (U/u = unmoved pawn, displays same as P/p)
+const PIECE_SYMBOLS = {
+  'K': '\u2654', 'Q': '\u2655', 'R': '\u2656', 'B': '\u2657', 'N': '\u2658', 'P': '\u2659', 'U': '\u2659',
+  'k': '\u265A', 'q': '\u265B', 'r': '\u265C', 'b': '\u265D', 'n': '\u265E', 'p': '\u265F', 'u': '\u265F'
+};
+
+const DISTORTION_NAMES = {
+  'slide': 'Slide (Azelf)',
+  'spin': 'Spin (Mesprit)',
+  'swap': 'Swap (Uxie)',
+  'void': 'Void (Giratina)'
+};
+
+const DISTORTION_INSTRUCTIONS = {
+  'slide': 'Select a tile to slide, then choose a direction. May push up to 2 additional tiles.',
+  'spin': 'Select a tile to rotate 90 degrees clockwise or counterclockwise.',
+  'swap': 'Select two adjacent tiles to swap their positions.',
+  'void': 'Select a tile to temporarily remove from play until the next distortion event.'
+};
+
+// ============= Game Logic (client-side) =============
+
+function createDeck() {
+  const deck = [];
+  for (let i = 0; i < 5; i++) deck.push('slide');
+  for (let i = 0; i < 3; i++) deck.push('spin');
+  for (let i = 0; i < 3; i++) deck.push('swap');
+  deck.push('void');
+  return shuffleDeck(deck);
+}
+
+function shuffleDeck(deck) {
+  const shuffled = [...deck];
+  for (let i = shuffled.length - 1; i > 0; i--) {
+    const j = Math.floor(Math.random() * (i + 1));
+    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+  }
+  return shuffled;
+}
+
+function createInitialTiles() {
+  const tiles = [];
+  for (let ty = 0; ty < 3; ty++) {
+    for (let tx = 0; tx < 3; tx++) {
+      tiles.push({
+        id: ty * 3 + tx,
+        x: 1 + tx * 3,
+        y: 1 + ty * 3,
+        rotation: 0,
+        voided: false
+      });
+    }
+  }
+  return tiles;
+}
+
+function createInitialBoard() {
+  const board = {};
+  const whiteBackRow = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
+  for (let x = 1; x <= 8; x++) {
+    board[`${x},1`] = whiteBackRow[x - 1];
+    board[`${x},2`] = 'U';
+  }
+  const blackBackRow = ['r', 'n', 'b', 'k', 'q', 'b', 'n', 'r'];
+  for (let x = 2; x <= 9; x++) {
+    board[`${x},9`] = blackBackRow[x - 2];
+    board[`${x},8`] = 'u';
+  }
+  return board;
+}
+
+function createBoulders() {
+  return [
+    { x: 1, y: 9 }, { x: 9, y: 1 },
+    { x: 1, y: 5 }, { x: 9, y: 5 },
+    { x: 5, y: 4 }, { x: 5, y: 6 }
+  ];
+}
+
+function createNewGame() {
+  return {
+    board: createInitialBoard(),
+    tiles: createInitialTiles(),
+    boulders: createBoulders(),
+    deck: createDeck(),
+    discard: [],
+    turn: 'white',
+    turnCount: 0,
+    phase: 'move',
+    pendingDistortion: null,
+    voidedTile: null,
+    captured: { white: [], black: [] },
+    gameOver: false,
+    winner: null,
+    lastMove: null
+  };
+}
+
+function getSquareTile(tiles, x, y) {
+  for (const tile of tiles) {
+    if (tile.voided) continue;
+    if (x >= tile.x && x < tile.x + 3 && y >= tile.y && y < tile.y + 3) {
+      return tile;
+    }
+  }
+  return null;
+}
+
+function isValidSquare(tiles, x, y) {
+  if (x < 0 || x > 10 || y < 0 || y > 10) return false;
+  return getSquareTile(tiles, x, y) !== null;
+}
+
+function isBoulderAt(boulders, x, y) {
+  return boulders.some(b => b.x === x && b.y === y);
+}
+
+function getPieceColor(piece) {
+  if (!piece) return null;
+  return piece === piece.toUpperCase() ? 'white' : 'black';
+}
+
+function getPieceType(piece) {
+  const upper = piece.toUpperCase();
+  if (upper === 'U') return 'P';
+  return upper;
+}
+
+function hasGap(tiles, x1, y1, x2, y2) {
+  const dx = Math.sign(x2 - x1);
+  const dy = Math.sign(y2 - y1);
+  let x = x1 + dx;
+  let y = y1 + dy;
+  while (x !== x2 || y !== y2) {
+    if (!getSquareTile(tiles, x, y)) return true;
+    x += dx;
+    y += dy;
+  }
+  if (!getSquareTile(tiles, x2, y2)) return true;
+  return false;
+}
+
+function findKing(game, color) {
+  const kingChar = color === 'white' ? 'K' : 'k';
+  for (const [key, piece] of Object.entries(game.board)) {
+    if (piece === kingChar) {
+      const [x, y] = key.split(',').map(Number);
+      return { x, y };
+    }
+  }
+  return null;
+}
+
+function getValidMoves(game, x, y, checkKingSafety = true) {
+  const piece = game.board[`${x},${y}`];
+  if (!piece) return [];
+  
+  const color = getPieceColor(piece);
+  const type = getPieceType(piece);
+  const moves = [];
+  
+  const tryAddMove = (tx, ty) => {
+    if (tx < 0 || tx > 10 || ty < 0 || ty > 10) return false;
+    if (!isValidSquare(game.tiles, tx, ty)) return false;
+    if (isBoulderAt(game.boulders, tx, ty)) return false;
+    
+    const targetPiece = game.board[`${tx},${ty}`];
+    if (targetPiece && getPieceColor(targetPiece) === color) return false;
+    
+    if (checkKingSafety) {
+      const testGame = JSON.parse(JSON.stringify(game));
+      testGame.board[`${tx},${ty}`] = piece;
+      delete testGame.board[`${x},${y}`];
+      if (isInCheck(testGame, color)) return false;
+    }
+    
+    moves.push({ x: tx, y: ty });
+    return !targetPiece;
+  };
+  
+  switch (type) {
+    case 'P': {
+      const dir = color === 'white' ? 1 : -1;
+      const isUnmoved = piece === 'U' || piece === 'u';
+      
+      const fy = y + dir;
+      if (isValidSquare(game.tiles, x, fy) && !game.board[`${x},${fy}`] && !isBoulderAt(game.boulders, x, fy)) {
+        if (!hasGap(game.tiles, x, y, x, fy)) {
+          let canMove = true;
+          if (checkKingSafety) {
+            const testGame = JSON.parse(JSON.stringify(game));
+            testGame.board[`${x},${fy}`] = piece;
+            delete testGame.board[`${x},${y}`];
+            if (isInCheck(testGame, color)) canMove = false;
+          }
+          if (canMove) {
+            moves.push({ x, y: fy });
+            if (isUnmoved) {
+              const fy2 = y + dir * 2;
+              if (isValidSquare(game.tiles, x, fy2) && !game.board[`${x},${fy2}`] && !isBoulderAt(game.boulders, x, fy2)) {
+                if (!hasGap(game.tiles, x, fy, x, fy2)) {
+                  let canMove2 = true;
+                  if (checkKingSafety) {
+                    const testGame = JSON.parse(JSON.stringify(game));
+                    testGame.board[`${x},${fy2}`] = piece;
+                    delete testGame.board[`${x},${y}`];
+                    if (isInCheck(testGame, color)) canMove2 = false;
+                  }
+                  if (canMove2) moves.push({ x, y: fy2 });
+                }
+              }
+            }
+          }
+        }
+      }
+      
+      for (const cx of [x - 1, x + 1]) {
+        const cy = y + dir;
+        if (cx >= 0 && cx <= 10 && cy >= 0 && cy <= 10) {
+          if (isValidSquare(game.tiles, cx, cy) && !hasGap(game.tiles, x, y, cx, cy)) {
+            const target = game.board[`${cx},${cy}`];
+            if (target && getPieceColor(target) !== color) {
+              let canCapture = true;
+              if (checkKingSafety) {
+                const testGame = JSON.parse(JSON.stringify(game));
+                testGame.board[`${cx},${cy}`] = piece;
+                delete testGame.board[`${x},${y}`];
+                if (isInCheck(testGame, color)) canCapture = false;
+              }
+              if (canCapture) moves.push({ x: cx, y: cy });
+            }
+          }
+        }
+      }
+      break;
+    }
+    
+    case 'N': {
+      const knightMoves = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
+      for (const [dx, dy] of knightMoves) tryAddMove(x + dx, y + dy);
+      break;
+    }
+    
+    case 'B': {
+      for (const [dx, dy] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
+        let prevX = x, prevY = y;
+        for (let i = 1; i <= 10; i++) {
+          const tx = x + dx * i, ty = y + dy * i;
+          if (tx < 0 || tx > 10 || ty < 0 || ty > 10) break;
+          if (!isValidSquare(game.tiles, tx, ty)) break;
+          if (hasGap(game.tiles, prevX, prevY, tx, ty)) break;
+          if (isBoulderAt(game.boulders, tx, ty)) break;
+          if (!tryAddMove(tx, ty)) break;
+          prevX = tx; prevY = ty;
+        }
+      }
+      break;
+    }
+    
+    case 'R': {
+      for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
+        let prevX = x, prevY = y;
+        for (let i = 1; i <= 10; i++) {
+          const tx = x + dx * i, ty = y + dy * i;
+          if (tx < 0 || tx > 10 || ty < 0 || ty > 10) break;
+          if (!isValidSquare(game.tiles, tx, ty)) break;
+          if (hasGap(game.tiles, prevX, prevY, tx, ty)) break;
+          if (isBoulderAt(game.boulders, tx, ty)) break;
+          if (!tryAddMove(tx, ty)) break;
+          prevX = tx; prevY = ty;
+        }
+      }
+      break;
+    }
+    
+    case 'Q': {
+      for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0],[-1,-1],[-1,1],[1,-1],[1,1]]) {
+        let blocked = false;
+        for (let i = 1; i <= 10; i++) {
+          const tx = x + dx * i, ty = y + dy * i;
+          if (tx < 0 || tx > 10 || ty < 0 || ty > 10) break;
+          if (!isValidSquare(game.tiles, tx, ty)) continue;
+          if (blocked) break;
+          if (isBoulderAt(game.boulders, tx, ty)) break;
+          if (!tryAddMove(tx, ty)) blocked = true;
+        }
+      }
+      break;
+    }
+    
+    case 'K': {
+      for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0],[-1,-1],[-1,1],[1,-1],[1,1]]) {
+        const tx = x + dx, ty = y + dy;
+        if (tx >= 0 && tx <= 10 && ty >= 0 && ty <= 10) {
+          if (isValidSquare(game.tiles, tx, ty) && !hasGap(game.tiles, x, y, tx, ty)) {
+            tryAddMove(tx, ty);
+          }
+        }
+      }
+      break;
+    }
+  }
+  
+  return moves;
+}
+
+function isInCheck(game, color) {
+  const kingPos = findKing(game, color);
+  if (!kingPos) return false;
+  
+  const opponentColor = color === 'white' ? 'black' : 'white';
+  for (const [key, piece] of Object.entries(game.board)) {
+    if (getPieceColor(piece) !== opponentColor) continue;
+    const [x, y] = key.split(',').map(Number);
+    const moves = getValidMoves(game, x, y, false);
+    if (moves.some(m => m.x === kingPos.x && m.y === kingPos.y)) return true;
+  }
+  return false;
+}
+
+function hasLegalMoves(game, color) {
+  for (const [key, piece] of Object.entries(game.board)) {
+    if (getPieceColor(piece) !== color) continue;
+    const [x, y] = key.split(',').map(Number);
+    if (getValidMoves(game, x, y, true).length > 0) return true;
+  }
+  return false;
+}
+
+function checkGameEnd(game) {
+  const color = game.turn;
+  if (!hasLegalMoves(game, color)) {
+    game.gameOver = true;
+    game.winner = isInCheck(game, color) ? (color === 'white' ? 'black' : 'white') : 'draw';
+  }
+}
+
+function makeMove(game, fromX, fromY, toX, toY, promotion = null) {
+  const piece = game.board[`${fromX},${fromY}`];
+  if (!piece) return { success: false, error: 'No piece at source' };
+  
+  const color = getPieceColor(piece);
+  if (color !== game.turn) return { success: false, error: 'Not your turn' };
+  if (game.phase !== 'move') return { success: false, error: 'Waiting for distortion' };
+  
+  const validMoves = getValidMoves(game, fromX, fromY, true);
+  if (!validMoves.some(m => m.x === toX && m.y === toY)) {
+    return { success: false, error: 'Invalid move' };
+  }
+  
+  const captured = game.board[`${toX},${toY}`];
+  if (captured) {
+    if (!game.captured) game.captured = { white: [], black: [] };
+    game.captured[color].push(captured);
+  }
+  
+  let newPiece = piece;
+  if (getPieceType(piece) === 'P') {
+    if (piece === 'U') newPiece = 'P';
+    if (piece === 'u') newPiece = 'p';
+    
+    const dir = color === 'white' ? 1 : -1;
+    const aheadY = toY + dir;
+    if (!isValidSquare(game.tiles, toX, aheadY)) {
+      const promoType = promotion || 'Q';
+      newPiece = color === 'white' ? promoType : promoType.toLowerCase();
+    }
+  }
+  
+  game.board[`${toX},${toY}`] = newPiece;
+  delete game.board[`${fromX},${fromY}`];
+  
+  game.lastMove = { fromX, fromY, toX, toY, piece, captured };
+  game.turnCount++;
+  
+  if (game.turnCount % 3 === 0) {
+    if (game.voidedTile !== null) {
+      const tile = game.tiles.find(t => t.id === game.voidedTile);
+      if (tile) tile.voided = false;
+      game.voidedTile = null;
+    }
+    
+    if (game.deck.length === 0) {
+      game.deck = shuffleDeck([...game.discard]);
+      game.discard = [];
+    }
+    
+    const card = game.deck.pop();
+    game.discard.push(card);
+    game.pendingDistortion = card;
+    game.phase = 'distortion';
+  } else {
+    game.turn = game.turn === 'white' ? 'black' : 'white';
+    checkGameEnd(game);
+  }
+  
+  return { success: true };
+}
+
+function applyDistortion(game, type, params) {
+  if (game.phase !== 'distortion') return { success: false, error: 'Not in distortion phase' };
+  if (type !== game.pendingDistortion) return { success: false, error: 'Wrong distortion type' };
+  
+  switch (type) {
+    case 'slide': {
+      const { tileId, direction, distance = 3 } = params;
+      const tile = game.tiles.find(t => t.id === tileId);
+      if (!tile || tile.voided) return { success: false, error: 'Invalid tile' };
+      if (distance < 1 || distance > 3) return { success: false, error: 'Invalid distance' };
+      
+      const unitDx = direction === 'left' ? -1 : direction === 'right' ? 1 : 0;
+      const unitDy = direction === 'down' ? -1 : direction === 'up' ? 1 : 0;
+      const dx = unitDx * distance, dy = unitDy * distance;
+      
+      const tilesOverlap = (t1x, t1y, t2x, t2y) => Math.abs(t1x - t2x) < 3 && Math.abs(t1y - t2y) < 3;
+      
+      const tilesToMove = [tile];
+      const tileIdsToMove = new Set([tile.id]);
+      
+      let changed = true;
+      while (changed) {
+        changed = false;
+        for (const movingTile of [...tilesToMove]) {
+          const newX = movingTile.x + dx, newY = movingTile.y + dy;
+          if (newX < 0 || newX > 8 || newY < 0 || newY > 8) return { success: false, error: 'Off board' };
+          
+          for (const otherTile of game.tiles) {
+            if (tileIdsToMove.has(otherTile.id) || otherTile.voided) continue;
+            if (tilesOverlap(newX, newY, otherTile.x, otherTile.y)) {
+              if (tilesToMove.length >= 3) return { success: false, error: 'Too many tiles' };
+              tilesToMove.push(otherTile);
+              tileIdsToMove.add(otherTile.id);
+              changed = true;
+            }
+          }
+        }
+      }
+      
+      for (const t of tilesToMove) {
+        if (t.x + dx < 0 || t.x + dx > 8 || t.y + dy < 0 || t.y + dy > 8) {
+          return { success: false, error: 'Off board' };
+        }
+      }
+      
+      for (let i = tilesToMove.length - 1; i >= 0; i--) {
+        const t = tilesToMove[i];
+        const newPositions = {};
+        const toDelete = [];
+        
+        for (const [key, p] of Object.entries(game.board)) {
+          const [px, py] = key.split(',').map(Number);
+          if (px >= t.x && px < t.x + 3 && py >= t.y && py < t.y + 3) {
+            newPositions[`${px + dx},${py + dy}`] = p;
+            toDelete.push(key);
+          }
+        }
+        for (const key of toDelete) delete game.board[key];
+        Object.assign(game.board, newPositions);
+        
+        for (const boulder of game.boulders) {
+          if (boulder.x >= t.x && boulder.x < t.x + 3 && boulder.y >= t.y && boulder.y < t.y + 3) {
+            boulder.x += dx;
+            boulder.y += dy;
+          }
+        }
+        t.x += dx;
+        t.y += dy;
+      }
+      break;
+    }
+    
+    case 'spin': {
+      const { tileId, clockwise } = params;
+      const tile = game.tiles.find(t => t.id === tileId);
+      if (!tile || tile.voided) return { success: false, error: 'Invalid tile' };
+      
+      const centerX = tile.x + 1, centerY = tile.y + 1;
+      const rotate = (ox, oy) => {
+        const relX = ox - centerX, relY = oy - centerY;
+        return clockwise 
+          ? { x: centerX + relY, y: centerY - relX }
+          : { x: centerX - relY, y: centerY + relX };
+      };
+      
+      const newPositions = {};
+      const toDelete = [];
+      for (const [key, p] of Object.entries(game.board)) {
+        const [px, py] = key.split(',').map(Number);
+        if (px >= tile.x && px < tile.x + 3 && py >= tile.y && py < tile.y + 3) {
+          const newPos = rotate(px, py);
+          newPositions[`${newPos.x},${newPos.y}`] = p;
+          toDelete.push(key);
+        }
+      }
+      for (const key of toDelete) delete game.board[key];
+      Object.assign(game.board, newPositions);
+      
+      for (const boulder of game.boulders) {
+        if (boulder.x >= tile.x && boulder.x < tile.x + 3 && boulder.y >= tile.y && boulder.y < tile.y + 3) {
+          const newPos = rotate(boulder.x, boulder.y);
+          boulder.x = newPos.x;
+          boulder.y = newPos.y;
+        }
+      }
+      tile.rotation = (tile.rotation + (clockwise ? 90 : -90) + 360) % 360;
+      break;
+    }
+    
+    case 'swap': {
+      const { tileId1, tileId2 } = params;
+      const tile1 = game.tiles.find(t => t.id === tileId1);
+      const tile2 = game.tiles.find(t => t.id === tileId2);
+      if (!tile1 || !tile2 || tile1.voided || tile2.voided) return { success: false, error: 'Invalid tiles' };
+      
+      const dx = tile2.x - tile1.x, dy = tile2.y - tile1.y;
+      
+      const pieces1 = [], pieces2 = [];
+      for (const [key, p] of Object.entries(game.board)) {
+        const [px, py] = key.split(',').map(Number);
+        if (px >= tile1.x && px < tile1.x + 3 && py >= tile1.y && py < tile1.y + 3) {
+          pieces1.push({ x: px, y: py, piece: p });
+        } else if (px >= tile2.x && px < tile2.x + 3 && py >= tile2.y && py < tile2.y + 3) {
+          pieces2.push({ x: px, y: py, piece: p });
+        }
+      }
+      
+      for (const { x, y } of pieces1) delete game.board[`${x},${y}`];
+      for (const { x, y } of pieces2) delete game.board[`${x},${y}`];
+      for (const { x, y, piece } of pieces1) game.board[`${x + dx},${y + dy}`] = piece;
+      for (const { x, y, piece } of pieces2) game.board[`${x - dx},${y - dy}`] = piece;
+      
+      const bouldersOnTile1 = game.boulders.filter(b => b.x >= tile1.x && b.x < tile1.x + 3 && b.y >= tile1.y && b.y < tile1.y + 3);
+      const bouldersOnTile2 = game.boulders.filter(b => b.x >= tile2.x && b.x < tile2.x + 3 && b.y >= tile2.y && b.y < tile2.y + 3);
+      for (const b of bouldersOnTile1) { b.x += dx; b.y += dy; }
+      for (const b of bouldersOnTile2) { b.x -= dx; b.y -= dy; }
+      
+      [tile1.x, tile2.x] = [tile2.x, tile1.x];
+      [tile1.y, tile2.y] = [tile2.y, tile1.y];
+      break;
+    }
+    
+    case 'void': {
+      const { tileId } = params;
+      const tile = game.tiles.find(t => t.id === tileId);
+      if (!tile || tile.voided) return { success: false, error: 'Invalid tile' };
+      
+      tile.voided = true;
+      game.voidedTile = tileId;
+      game.boulders = game.boulders.filter(b => !(b.x >= tile.x && b.x < tile.x + 3 && b.y >= tile.y && b.y < tile.y + 3));
+      break;
+    }
+    
+    default:
+      return { success: false, error: 'Unknown distortion' };
+  }
+  
+  game.phase = 'move';
+  game.pendingDistortion = null;
+  game.turn = game.turn === 'white' ? 'black' : 'white';
+  checkGameEnd(game);
+  
+  return { success: true };
+}
+
+class DistortionChess {
+  constructor() {
+    this.ws = null;
+    this.game = null;
+    this.gameState = null;
+    this.selectedSquare = null;
+    this.validMoves = [];
+    this.distortionStep = 0;
+    this.distortionSelection = {};
+    this.pendingPromotion = null;
+    this.previewDistortion = null;
+    this.isCreator = false;
+    this.playerColor = null;
+    this.gameId = null;
+    this.hasOpponent = false;
+    
+    this.init();
+  }
+  
+  init() {
+    this.connectWebSocket();
+    this.setupEventListeners();
+    this.setupChatListeners();
+  }
+  
+  connectWebSocket() {
+    const urlParams = new URLSearchParams(window.location.search);
+    let gameId = urlParams.get('game');
+    const color = urlParams.get('color');
+    
+    if (!gameId) {
+      gameId = Math.random().toString(36).substring(2, 10);
+      this.isCreator = true;
+    }
+    
+    this.gameId = gameId;
+    this.playerColor = color || (this.isCreator ? 'white' : null);
+    
+    const wsUrl = `wss://ws.za3k.com/distortionchess-${gameId}`;
+    
+    this.ws = new WebSocket(wsUrl);
+    
+    this.ws.onopen = () => {
+      console.log('Connected to relay:', wsUrl);
+      
+      if (this.isCreator) {
+        this.updateUrlWithGame(gameId, 'white');
+        this.initializeNewGame();
+        this.updateInviteUrl();
+      } else if (color === 'white') {
+        this.initializeNewGame();
+        this.updateInviteUrl();
+      } else if (color === 'black') {
+        this.ws.send(JSON.stringify({ type: 'requestSync', color, sender: color }));
+        setTimeout(() => {
+          if (!this.game) {
+            console.log('No sync received, white may not be online');
+          }
+        }, 3000);
+      }
+    };
+    
+    this.ws.onmessage = (event) => {
+      const msg = JSON.parse(event.data);
+      this.handleMessage(msg);
+    };
+    
+    this.ws.onclose = () => {
+      console.log('Disconnected from relay');
+      setTimeout(() => this.connectWebSocket(), 3000);
+    };
+    
+    this.ws.onerror = (e) => {
+      console.error('WebSocket error:', e);
+    };
+  }
+  
+  initializeNewGame() {
+    this.game = createNewGame();
+    this.playerColor = 'white';
+    this.updateGameState();
+  }
+  
+  updateGameState() {
+    if (!this.game) return;
+    
+    this.gameState = {
+      board: this.game.board,
+      tiles: this.game.tiles,
+      boulders: this.game.boulders,
+      turn: this.game.turn,
+      turnCount: this.game.turnCount,
+      turnsUntilDistortion: 3 - (this.game.turnCount % 3),
+      phase: this.game.phase,
+      pendingDistortion: this.game.pendingDistortion,
+      voidedTile: this.game.voidedTile,
+      playerColor: this.playerColor,
+      captured: this.game.captured || { white: [], black: [] },
+      gameOver: this.game.gameOver,
+      winner: this.game.winner,
+      lastMove: this.game.lastMove,
+      inCheck: isInCheck(this.game, this.game.turn),
+      hasBlackPlayer: this.hasOpponent
+    };
+    
+    this.renderBoard();
+    this.renderCaptured();
+    this.updateUI();
+  }
+  
+  handleMessage(msg) {
+    if (msg.sender === this.playerColor) return;
+    
+    switch (msg.type) {
+      case 'requestSync':
+        if (this.game && this.playerColor === 'white') {
+          this.hasOpponent = true;
+          this.ws.send(JSON.stringify({
+            type: 'sync',
+            game: this.game,
+            sender: this.playerColor
+          }));
+          this.updateGameState();
+        }
+        break;
+        
+      case 'sync':
+        this.game = msg.game;
+        this.hasOpponent = true;
+        this.updateGameState();
+        break;
+        
+      case 'move':
+        if (this.game) {
+          const result = makeMove(this.game, msg.fromX, msg.fromY, msg.toX, msg.toY, msg.promotion);
+          if (result.success) {
+            this.updateGameState();
+          }
+        }
+        break;
+        
+      case 'distortion':
+        if (this.game) {
+          const result = applyDistortion(this.game, msg.distortionType, msg.params);
+          if (result.success) {
+            this.updateGameState();
+          }
+        }
+        break;
+        
+      case 'chat':
+        const senderName = msg.sender === 'white' ? 'White' : 'Black';
+        this.addChatMessage(senderName, msg.message, false);
+        break;
+    }
+  }
+  
+  renderCaptured() {
+    if (!this.gameState || !this.gameState.captured) return;
+    
+    const topCaptured = document.getElementById('captured-black');
+    topCaptured.innerHTML = '';
+    for (const piece of this.gameState.captured.black) {
+      const span = document.createElement('span');
+      span.className = 'piece white';
+      span.textContent = PIECE_SYMBOLS[piece] || piece;
+      topCaptured.appendChild(span);
+    }
+    
+    const bottomCaptured = document.getElementById('captured-white');
+    bottomCaptured.innerHTML = '';
+    for (const piece of this.gameState.captured.white) {
+      const span = document.createElement('span');
+      span.className = 'piece black';
+      span.textContent = PIECE_SYMBOLS[piece] || piece;
+      bottomCaptured.appendChild(span);
+    }
+  }
+  
+  getBaseUrl() {
+    // Keep the full pathname including the HTML file
+    return `${window.location.origin}${window.location.pathname}`;
+  }
+  
+  updateUrlWithGame(gameId, color) {
+    const fullPath = window.location.pathname;
+    window.history.replaceState({}, '', `${fullPath}?game=${gameId}&color=${color}`);
+  }
+  
+  updateInviteUrl() {
+    const baseUrl = this.getBaseUrl();
+    const url = `${baseUrl}?game=${this.gameId}&color=black`;
+    document.getElementById('invite-url').value = url;
+  }
+  
+  setupEventListeners() {
+    document.getElementById('copy-url').addEventListener('click', () => {
+      const input = document.getElementById('invite-url');
+      input.select();
+      input.setSelectionRange(0, 99999);
+      
+      try {
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+          navigator.clipboard.writeText(input.value);
+        } else {
+          document.execCommand('copy');
+        }
+      } catch (e) {}
+    });
+  }
+  
+  setupChatListeners() {
+    const chatToggle = document.getElementById('chat-toggle');
+    const chatContainer = document.getElementById('chat-container');
+    const chatInput = document.getElementById('chat-input');
+    const chatSend = document.getElementById('chat-send');
+    
+    chatToggle.addEventListener('click', (e) => {
+      e.stopPropagation();
+      chatContainer.classList.toggle('minimized');
+      chatToggle.textContent = chatContainer.classList.contains('minimized') ? '+' : '-';
+    });
+    
+    document.getElementById('chat-header').addEventListener('click', () => {
+      chatContainer.classList.toggle('minimized');
+      chatToggle.textContent = chatContainer.classList.contains('minimized') ? '+' : '-';
+    });
+    
+    const sendMessage = () => {
+      const text = chatInput.value.trim();
+      if (!text || !this.ws || this.ws.readyState !== WebSocket.OPEN) return;
+      
+      this.ws.send(JSON.stringify({
+        type: 'chat',
+        message: text,
+        sender: this.playerColor
+      }));
+      const senderName = this.playerColor === 'white' ? 'White' : 'Black';
+      this.addChatMessage(senderName, text, true);
+      chatInput.value = '';
+    };
+    
+    chatSend.addEventListener('click', sendMessage);
+    chatInput.addEventListener('keypress', (e) => {
+      if (e.key === 'Enter') sendMessage();
+    });
+  }
+  
+  addChatMessage(sender, text, isMine) {
+    const messagesEl = document.getElementById('chat-messages');
+    const msgEl = document.createElement('div');
+    msgEl.className = 'chat-message ' + (isMine ? 'mine' : 'theirs');
+    
+    const senderEl = document.createElement('div');
+    senderEl.className = 'sender';
+    senderEl.textContent = sender;
+    
+    const textEl = document.createElement('div');
+    textEl.className = 'text';
+    textEl.textContent = text;
+    
+    msgEl.appendChild(senderEl);
+    msgEl.appendChild(textEl);
+    messagesEl.appendChild(msgEl);
+    
+    messagesEl.scrollTop = messagesEl.scrollHeight;
+    
+    const chatContainer = document.getElementById('chat-container');
+    if (chatContainer.classList.contains('minimized') && !isMine) {
+      chatContainer.classList.add('new-message');
+      setTimeout(() => chatContainer.classList.remove('new-message'), 2000);
+    }
+  }
+  
+  getTileAt(x, y) {
+    if (!this.gameState) return null;
+    for (const tile of this.gameState.tiles) {
+      if (tile.voided) continue;
+      if (x >= tile.x && x < tile.x + 3 && y >= tile.y && y < tile.y + 3) {
+        return tile;
+      }
+    }
+    return null;
+  }
+  
+  isBoulder(x, y) {
+    if (!this.gameState) return false;
+    return this.gameState.boulders.some(b => b.x === x && b.y === y);
+  }
+  
+  isVoidedTileSquare(x, y) {
+    if (!this.gameState) return false;
+    for (const tile of this.gameState.tiles) {
+      if (tile.voided && x >= tile.x && x < tile.x + 3 && y >= tile.y && y < tile.y + 3) {
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  renderBoard() {
+    const board = document.getElementById('board');
+    board.innerHTML = '';
+    
+    if (!this.gameState) return;
+    
+    for (let visualY = 10; visualY >= 0; visualY--) {
+      for (let x = 0; x <= 10; x++) {
+        const square = document.createElement('div');
+        square.className = 'square';
+        
+        const gameX = x;
+        const gameY = visualY;
+        
+        const preview = this.previewDistortion ? this.getPreviewPosition(gameX, gameY) : null;
+        const hideOriginal = preview && preview.hideOriginal;
+        
+        const tile = this.getTileAt(gameX, gameY);
+        const isVoidedSquare = this.isVoidedTileSquare(gameX, gameY);
+        
+        if (isVoidedSquare) {
+          square.classList.add('voided-tile');
+        } else if (tile && !hideOriginal) {
+          square.classList.add('tile');
+          
+          if ((gameX + gameY) % 2 === 0) {
+            square.classList.add('light');
+          } else {
+            square.classList.add('dark');
+          }
+          
+          if (gameY === tile.y + 2) square.classList.add('tile-border-top');
+          if (gameY === tile.y) square.classList.add('tile-border-bottom');
+          if (gameX === tile.x) square.classList.add('tile-border-left');
+          if (gameX === tile.x + 2) square.classList.add('tile-border-right');
+        } else if (hideOriginal) {
+          square.classList.add('void');
+        } else if (preview && preview.ghostTile) {
+          square.classList.add('ghost-tile');
+          if ((gameX + gameY) % 2 === 0) {
+            square.classList.add('light');
+          } else {
+            square.classList.add('dark');
+          }
+        } else {
+          square.classList.add('void');
+        }
+        
+        if (this.isBoulder(gameX, gameY) && !hideOriginal) {
+          square.classList.add('boulder');
+        }
+        
+        const piece = this.gameState.board[`${gameX},${gameY}`];
+        if (piece && !isVoidedSquare && !hideOriginal) {
+          const pieceEl = document.createElement('span');
+          pieceEl.className = 'piece ' + (piece === piece.toUpperCase() ? 'white' : 'black');
+          pieceEl.textContent = PIECE_SYMBOLS[piece];
+          square.appendChild(pieceEl);
+          
+          if (piece.toUpperCase() === 'K' && this.gameState.inCheck) {
+            const kingColor = piece === 'K' ? 'white' : 'black';
+            if (kingColor === this.gameState.turn) {
+              square.classList.add('check');
+            }
+          }
+        }
+        
+        if (preview) {
+          if (preview.ghostTile) {
+            if (!square.classList.contains('ghost-tile')) {
+              square.classList.add('ghost-tile');
+            }
+            
+            if (preview.boulder) {
+              square.classList.add('ghost-boulder');
+            }
+            
+            if (preview.piece) {
+              const ghostEl = document.createElement('span');
+              ghostEl.className = 'piece ghost ' + (preview.piece === preview.piece.toUpperCase() ? 'white' : 'black');
+              ghostEl.textContent = PIECE_SYMBOLS[preview.piece];
+              square.appendChild(ghostEl);
+            }
+          }
+          if (preview.willVoid) {
+            square.classList.add('will-void');
+          }
+        }
+        
+        if (this.selectedSquare && this.selectedSquare.x === gameX && this.selectedSquare.y === gameY) {
+          square.classList.add('selected');
+        }
+        
+        const isValidMove = this.validMoves.some(m => m.x === gameX && m.y === gameY);
+        if (isValidMove) {
+          if (this.gameState.board[`${gameX},${gameY}`]) {
+            square.classList.add('valid-capture');
+          } else {
+            square.classList.add('valid-move');
+          }
+        }
+        
+        if (this.gameState.lastMove) {
+          const lm = this.gameState.lastMove;
+          if ((gameX === lm.fromX && gameY === lm.fromY) || (gameX === lm.toX && gameY === lm.toY)) {
+            square.classList.add('last-move');
+          }
+        }
+        
+        if (this.gameState.phase === 'distortion' && tile && !isVoidedSquare && !hideOriginal) {
+          if (this.distortionSelection.tile1 === tile.id || this.distortionSelection.tile2 === tile.id) {
+            square.classList.add('tile-selected');
+          }
+        }
+        
+        square.dataset.x = gameX;
+        square.dataset.y = gameY;
+        square.addEventListener('click', (e) => this.handleSquareClick(gameX, gameY));
+        
+        board.appendChild(square);
+      }
+    }
+  }
+  
+  handleSquareClick(x, y) {
+    if (!this.gameState || this.gameState.gameOver) return;
+    
+    if (this.gameState.phase === 'distortion') {
+      this.handleDistortionClick(x, y);
+      return;
+    }
+    
+    if (this.gameState.playerColor !== this.gameState.turn) return;
+    
+    const tile = this.getTileAt(x, y);
+    if (!tile) return;
+    
+    const piece = this.gameState.board[`${x},${y}`];
+    const pieceColor = piece ? (piece === piece.toUpperCase() ? 'white' : 'black') : null;
+    
+    if (piece && pieceColor === this.gameState.playerColor) {
+      if (this.selectedSquare && this.selectedSquare.x === x && this.selectedSquare.y === y) {
+        this.selectedSquare = null;
+        this.validMoves = [];
+        this.renderBoard();
+      } else {
+        this.selectedSquare = { x, y };
+        this.validMoves = getValidMoves(this.game, x, y, true);
+        this.renderBoard();
+      }
+      return;
+    }
+    
+    if (this.selectedSquare && this.validMoves.some(m => m.x === x && m.y === y)) {
+      const movingPiece = this.gameState.board[`${this.selectedSquare.x},${this.selectedSquare.y}`];
+      const isPawn = movingPiece && (movingPiece.toUpperCase() === 'P' || movingPiece.toUpperCase() === 'U');
+      
+      if (isPawn) {
+        const dir = this.gameState.playerColor === 'white' ? 1 : -1;
+        const aheadY = y + dir;
+        const canAdvanceFurther = this.getTileAt(x, aheadY) !== null;
+        
+        if (!canAdvanceFurther) {
+          this.pendingPromotion = { fromX: this.selectedSquare.x, fromY: this.selectedSquare.y, toX: x, toY: y };
+          this.showPromotionDialog();
+          return;
+        }
+      }
+      
+      this.sendMove(this.selectedSquare.x, this.selectedSquare.y, x, y);
+      return;
+    }
+    
+    this.selectedSquare = null;
+    this.validMoves = [];
+    this.renderBoard();
+  }
+  
+  handleDistortionClick(x, y) {
+    if (this.gameState.playerColor !== this.gameState.turn) return;
+    
+    const tile = this.getTileAt(x, y);
+    if (!tile) return;
+    
+    const distType = this.gameState.pendingDistortion;
+    
+    switch (distType) {
+      case 'slide':
+      case 'spin':
+      case 'void':
+        if (this.distortionSelection.tile1 === tile.id) {
+          this.distortionSelection = {};
+          this.previewDistortion = null;
+        } else {
+          this.distortionSelection.tile1 = tile.id;
+        }
+        this.renderBoard();
+        this.updateDistortionControls();
+        break;
+        
+      case 'swap':
+        if (this.distortionSelection.tile1 === undefined) {
+          this.distortionSelection.tile1 = tile.id;
+        } else if (this.distortionSelection.tile1 === tile.id) {
+          this.distortionSelection = {};
+          this.previewDistortion = null;
+        } else if (this.distortionSelection.tile2 === tile.id) {
+          this.distortionSelection.tile2 = undefined;
+          this.previewDistortion = null;
+        } else {
+          const tile1 = this.gameState.tiles.find(t => t.id === this.distortionSelection.tile1);
+          if (this.isSwapValid(tile1, tile)) {
+            this.distortionSelection.tile2 = tile.id;
+            this.previewDistortion = { type: 'swap', tileId1: tile1.id, tileId2: tile.id };
+          }
+        }
+        this.renderBoard();
+        this.updateDistortionControls();
+        break;
+    }
+  }
+  
+  sendMove(fromX, fromY, toX, toY, promotion = null) {
+    this.selectedSquare = null;
+    this.validMoves = [];
+    
+    const result = makeMove(this.game, fromX, fromY, toX, toY, promotion);
+    if (result.success) {
+      this.ws.send(JSON.stringify({
+        type: 'move',
+        fromX, fromY, toX, toY,
+        promotion,
+        sender: this.playerColor
+      }));
+      this.updateGameState();
+    } else {
+      console.error('Move failed:', result.error);
+    }
+  }
+  
+  showPromotionDialog() {
+    const dialog = document.getElementById('promotion-dialog');
+    const options = document.getElementById('promotion-options');
+    options.innerHTML = '';
+    
+    const pieces = ['Q', 'R', 'B', 'N'];
+    const color = this.gameState.playerColor;
+    
+    pieces.forEach(p => {
+      const btn = document.createElement('button');
+      const symbol = color === 'white' ? PIECE_SYMBOLS[p] : PIECE_SYMBOLS[p.toLowerCase()];
+      btn.textContent = symbol;
+      btn.addEventListener('click', () => {
+        const promo = this.pendingPromotion;
+        this.sendMove(promo.fromX, promo.fromY, promo.toX, promo.toY, p);
+        this.pendingPromotion = null;
+        dialog.classList.add('hidden');
+      });
+      options.appendChild(btn);
+    });
+    
+    dialog.classList.remove('hidden');
+  }
+  
+  sendDistortion(params) {
+    const distType = this.gameState.pendingDistortion;
+    const result = applyDistortion(this.game, distType, params);
+    if (result.success) {
+      this.ws.send(JSON.stringify({
+        type: 'distortion',
+        distortionType: distType,
+        params,
+        sender: this.playerColor
+      }));
+      this.updateGameState();
+    } else {
+      console.error('Distortion failed:', result.error);
+    }
+    this.distortionStep = 0;
+    this.distortionSelection = {};
+    this.previewDistortion = null;
+  }
+  
+  updateUI() {
+    if (!this.gameState) return;
+    
+    const isMyTurn = this.gameState.playerColor === this.gameState.turn;
+    
+    const turnEl = document.getElementById('turn-indicator');
+    if (isMyTurn) {
+      turnEl.textContent = 'YOUR TURN';
+      turnEl.className = this.gameState.turn + ' my-turn';
+    } else {
+      turnEl.textContent = `${this.gameState.turn.charAt(0).toUpperCase() + this.gameState.turn.slice(1)}'s Turn`;
+      turnEl.className = this.gameState.turn;
+    }
+    
+    const counterEl = document.getElementById('distortion-counter');
+    counterEl.textContent = `Distortion in ${this.gameState.turnsUntilDistortion} turn${this.gameState.turnsUntilDistortion !== 1 ? 's' : ''}`;
+    
+    const colorEl = document.getElementById('player-color');
+    if (this.gameState.playerColor) {
+      colorEl.textContent = `Playing as ${this.gameState.playerColor.charAt(0).toUpperCase() + this.gameState.playerColor.slice(1)}`;
+    } else {
+      colorEl.textContent = 'Spectating';
+    }
+    
+    const statusEl = document.getElementById('game-status');
+    if (this.gameState.phase === 'distortion') {
+      if (isMyTurn) {
+        statusEl.textContent = `Distortion Event: ${DISTORTION_NAMES[this.gameState.pendingDistortion]}`;
+      } else {
+        statusEl.textContent = `Distortion Event (waiting for opponent)`;
+      }
+    } else if (isMyTurn) {
+      statusEl.textContent = 'Your turn to move';
+    } else {
+      statusEl.textContent = 'Waiting for opponent...';
+    }
+    
+    const invitePanel = document.getElementById('invite-panel');
+    if (this.gameState.playerColor === 'black' || this.gameState.hasBlackPlayer) {
+      invitePanel.classList.add('hidden');
+    } else {
+      invitePanel.classList.remove('hidden');
+    }
+    
+    const checkEl = document.getElementById('check-indicator');
+    if (this.gameState.inCheck) {
+      checkEl.textContent = 'CHECK!';
+    } else {
+      checkEl.textContent = '';
+    }
+    
+    const distPanel = document.getElementById('distortion-panel');
+    if (this.gameState.phase === 'distortion') {
+      distPanel.classList.remove('hidden');
+      if (isMyTurn) {
+        document.getElementById('distortion-card').textContent = DISTORTION_NAMES[this.gameState.pendingDistortion];
+        document.getElementById('distortion-instructions').textContent = DISTORTION_INSTRUCTIONS[this.gameState.pendingDistortion];
+        if (!this._updatingControls) {
+          this._updatingControls = true;
+          this.updateDistortionControls();
+          this._updatingControls = false;
+        }
+      } else {
+        document.getElementById('distortion-card').textContent = DISTORTION_NAMES[this.gameState.pendingDistortion];
+        document.getElementById('distortion-instructions').textContent = 'Waiting for opponent to choose...';
+        document.getElementById('distortion-controls').innerHTML = '';
+      }
+    } else {
+      distPanel.classList.add('hidden');
+      this.distortionSelection = {};
+    }
+    
+    if (this.gameState.gameOver) {
+      this.showGameOver();
+    }
+  }
+  
+  isSlideValid(tile, direction, distance) {
+    const unitDx = direction === 'left' ? -1 : direction === 'right' ? 1 : 0;
+    const unitDy = direction === 'down' ? -1 : direction === 'up' ? 1 : 0;
+    const dx = unitDx * distance;
+    const dy = unitDy * distance;
+    
+    const newX = tile.x + dx;
+    const newY = tile.y + dy;
+    if (newX < 0 || newX > 8 || newY < 0 || newY > 8) return false;
+    
+    const tilesToMove = [tile];
+    const tileIdsToMove = new Set([tile.id]);
+    
+    let changed = true;
+    while (changed) {
+      changed = false;
+      for (const movingTile of [...tilesToMove]) {
+        const mtNewX = movingTile.x + dx;
+        const mtNewY = movingTile.y + dy;
+        
+        if (mtNewX < 0 || mtNewX > 8 || mtNewY < 0 || mtNewY > 8) return false;
+        
+        for (const t of this.gameState.tiles) {
+          if (tileIdsToMove.has(t.id) || t.voided) continue;
+          if (Math.abs(mtNewX - t.x) < 3 && Math.abs(mtNewY - t.y) < 3) {
+            if (tilesToMove.length >= 3) return false;
+            tilesToMove.push(t);
+            tileIdsToMove.add(t.id);
+            changed = true;
+          }
+        }
+      }
+    }
+    
+    for (const t of tilesToMove) {
+      const finalX = t.x + dx;
+      const finalY = t.y + dy;
+      if (finalX < 0 || finalX > 8 || finalY < 0 || finalY > 8) return false;
+    }
+    
+    return true;
+  }
+  
+  isSwapValid(tile1, tile2) {
+    const t1Right = tile1.x + 3, t2Right = tile2.x + 3;
+    const t1Top = tile1.y + 3, t2Top = tile2.y + 3;
+    
+    const sharesVerticalEdge = (t1Right === tile2.x || t2Right === tile1.x) &&
+                                (tile1.y < t2Top && tile2.y < t1Top);
+    const sharesHorizontalEdge = (t1Top === tile2.y || t2Top === tile1.y) &&
+                                  (tile1.x < t2Right && tile2.x < t1Right);
+    
+    return sharesVerticalEdge || sharesHorizontalEdge;
+  }
+  
+  getSlideTiles(tile, direction, distance) {
+    const unitDx = direction === 'left' ? -1 : direction === 'right' ? 1 : 0;
+    const unitDy = direction === 'down' ? -1 : direction === 'up' ? 1 : 0;
+    const dx = unitDx * distance;
+    const dy = unitDy * distance;
+    
+    const tilesToMove = [tile];
+    const tileIdsToMove = new Set([tile.id]);
+    
+    let changed = true;
+    while (changed) {
+      changed = false;
+      for (const movingTile of [...tilesToMove]) {
+        const mtNewX = movingTile.x + dx;
+        const mtNewY = movingTile.y + dy;
+        
+        for (const t of this.gameState.tiles) {
+          if (tileIdsToMove.has(t.id) || t.voided) continue;
+          if (Math.abs(mtNewX - t.x) < 3 && Math.abs(mtNewY - t.y) < 3) {
+            tilesToMove.push(t);
+            tileIdsToMove.add(t.id);
+            changed = true;
+          }
+        }
+      }
+    }
+    return { tiles: tilesToMove, dx, dy };
+  }
+  
+  getPreviewPosition(x, y) {
+    if (!this.previewDistortion) return null;
+    
+    const preview = this.previewDistortion;
+    
+    if (preview.type === 'slide') {
+      const tile = this.gameState.tiles.find(t => t.id === preview.tileId);
+      if (!tile) return null;
+      
+      const { tiles: tilesToMove, dx, dy } = this.getSlideTiles(tile, preview.direction, preview.distance);
+      
+      let inOriginal = false;
+      for (const t of tilesToMove) {
+        if (x >= t.x && x < t.x + 3 && y >= t.y && y < t.y + 3) {
+          inOriginal = true;
+          break;
+        }
+      }
+      
+      for (const t of tilesToMove) {
+        const newTileX = t.x + dx;
+        const newTileY = t.y + dy;
+        if (x >= newTileX && x < newTileX + 3 && y >= newTileY && y < newTileY + 3) {
+          const origX = x - dx;
+          const origY = y - dy;
+          const piece = this.gameState.board[`${origX},${origY}`];
+          const boulder = this.gameState.boulders.find(b => b.x === origX && b.y === origY);
+          return { ghostTile: true, piece, boulder, hideOriginal: true };
+        }
+      }
+      
+      if (inOriginal) {
+        return { hideOriginal: true };
+      }
+    }
+    
+    if (preview.type === 'spin') {
+      const tile = this.gameState.tiles.find(t => t.id === preview.tileId);
+      if (!tile) return null;
+      
+      if (x >= tile.x && x < tile.x + 3 && y >= tile.y && y < tile.y + 3) {
+        const centerX = tile.x + 1;
+        const centerY = tile.y + 1;
+        const relX = x - centerX;
+        const relY = y - centerY;
+        
+        let origX, origY;
+        if (preview.clockwise) {
+          origX = centerX - relY;
+          origY = centerY + relX;
+        } else {
+          origX = centerX + relY;
+          origY = centerY - relX;
+        }
+        
+        const piece = this.gameState.board[`${origX},${origY}`];
+        const boulder = this.gameState.boulders.find(b => b.x === origX && b.y === origY);
+        return { ghostTile: true, piece, boulder, hideOriginal: true };
+      }
+    }
+    
+    if (preview.type === 'swap') {
+      const tile1 = this.gameState.tiles.find(t => t.id === preview.tileId1);
+      const tile2 = this.gameState.tiles.find(t => t.id === preview.tileId2);
+      if (!tile1 || !tile2) return null;
+      
+      const dx = tile2.x - tile1.x;
+      const dy = tile2.y - tile1.y;
+      
+      if (x >= tile1.x && x < tile1.x + 3 && y >= tile1.y && y < tile1.y + 3) {
+        const origX = x + dx;
+        const origY = y + dy;
+        const piece = this.gameState.board[`${origX},${origY}`];
+        const boulder = this.gameState.boulders.find(b => b.x === origX && b.y === origY);
+        return { ghostTile: true, piece, boulder, hideOriginal: true };
+      }
+      
+      if (x >= tile2.x && x < tile2.x + 3 && y >= tile2.y && y < tile2.y + 3) {
+        const origX = x - dx;
+        const origY = y - dy;
+        const piece = this.gameState.board[`${origX},${origY}`];
+        const boulder = this.gameState.boulders.find(b => b.x === origX && b.y === origY);
+        return { ghostTile: true, piece, boulder, hideOriginal: true };
+      }
+    }
+    
+    if (preview.type === 'void') {
+      const tile = this.gameState.tiles.find(t => t.id === preview.tileId);
+      if (tile && x >= tile.x && x < tile.x + 3 && y >= tile.y && y < tile.y + 3) {
+        return { willVoid: true };
+      }
+    }
+    
+    return null;
+  }
+
+  updateDistortionControls() {
+    const controls = document.getElementById('distortion-controls');
+    controls.innerHTML = '';
+    
+    const distType = this.gameState.pendingDistortion;
+    const hasTile1 = this.distortionSelection.tile1 !== undefined;
+    const hasTile2 = this.distortionSelection.tile2 !== undefined;
+    
+    switch (distType) {
+      case 'slide':
+        if (hasTile1) {
+          const tile = this.gameState.tiles.find(t => t.id === this.distortionSelection.tile1);
+          ['up', 'down', 'left', 'right'].forEach(dir => {
+            const dirDiv = document.createElement('div');
+            dirDiv.className = 'direction-group';
+            dirDiv.innerHTML = `<span>${dir.charAt(0).toUpperCase() + dir.slice(1)}:</span>`;
+            [1, 2, 3].forEach(dist => {
+              const btn = document.createElement('button');
+              btn.textContent = dist;
+              btn.className = 'dist-btn';
+              
+              const valid = this.isSlideValid(tile, dir, dist);
+              if (!valid) {
+                btn.disabled = true;
+                btn.classList.add('disabled');
+              } else {
+                btn.addEventListener('mouseenter', () => {
+                  this.previewDistortion = { type: 'slide', tileId: tile.id, direction: dir, distance: dist };
+                  this.renderBoard();
+                });
+                btn.addEventListener('mouseleave', () => {
+                  this.previewDistortion = null;
+                  this.renderBoard();
+                });
+                btn.addEventListener('click', () => {
+                  this.sendDistortion({ tileId: tile.id, direction: dir, distance: dist });
+                });
+              }
+              dirDiv.appendChild(btn);
+            });
+            controls.appendChild(dirDiv);
+          });
+        } else {
+          controls.innerHTML = '<p>Click a tile to select it</p>';
+        }
+        break;
+        
+      case 'spin':
+        if (hasTile1) {
+          const tile = this.gameState.tiles.find(t => t.id === this.distortionSelection.tile1);
+          
+          const cwBtn = document.createElement('button');
+          cwBtn.textContent = 'Clockwise';
+          cwBtn.addEventListener('mouseenter', () => {
+            this.previewDistortion = { type: 'spin', tileId: tile.id, clockwise: true };
+            this.renderBoard();
+          });
+          cwBtn.addEventListener('mouseleave', () => {
+            this.previewDistortion = null;
+            this.renderBoard();
+          });
+          cwBtn.addEventListener('click', () => {
+            this.sendDistortion({ tileId: tile.id, clockwise: true });
+          });
+          controls.appendChild(cwBtn);
+          
+          const ccwBtn = document.createElement('button');
+          ccwBtn.textContent = 'Counter-CW';
+          ccwBtn.addEventListener('mouseenter', () => {
+            this.previewDistortion = { type: 'spin', tileId: tile.id, clockwise: false };
+            this.renderBoard();
+          });
+          ccwBtn.addEventListener('mouseleave', () => {
+            this.previewDistortion = null;
+            this.renderBoard();
+          });
+          ccwBtn.addEventListener('click', () => {
+            this.sendDistortion({ tileId: tile.id, clockwise: false });
+          });
+          controls.appendChild(ccwBtn);
+        } else {
+          controls.innerHTML = '<p>Click a tile to select it</p>';
+        }
+        break;
+        
+      case 'swap':
+        if (hasTile1 && hasTile2) {
+          const tile1 = this.gameState.tiles.find(t => t.id === this.distortionSelection.tile1);
+          const tile2 = this.gameState.tiles.find(t => t.id === this.distortionSelection.tile2);
+          if (this.isSwapValid(tile1, tile2)) {
+            this.previewDistortion = { type: 'swap', tileId1: tile1.id, tileId2: tile2.id };
+            this.renderBoard();
+            
+            controls.innerHTML = '<p>Swapping tiles ' + (tile1.id + 1) + ' and ' + (tile2.id + 1) + '</p>';
+            const submitBtn = document.createElement('button');
+            submitBtn.textContent = 'Confirm Swap';
+            submitBtn.className = 'submit-btn';
+            submitBtn.addEventListener('click', () => {
+              const t1 = tile1.id, t2 = tile2.id;
+              this.distortionSelection = {};
+              this.previewDistortion = null;
+              this.sendDistortion({ tileId1: t1, tileId2: t2 });
+            });
+            controls.appendChild(submitBtn);
+          } else {
+            controls.innerHTML = '<p>Tiles not adjacent. Click a different tile.</p>';
+            this.distortionSelection.tile2 = undefined;
+          }
+        } else if (hasTile1) {
+          controls.innerHTML = '<p>Click another tile to swap with</p>';
+        } else {
+          controls.innerHTML = '<p>Click a tile to select it</p>';
+        }
+        break;
+        
+      case 'void':
+        if (hasTile1) {
+          this.sendDistortion({ tileId: this.distortionSelection.tile1 });
+        } else {
+          controls.innerHTML = '<p>Click a tile to void it</p>';
+        }
+        break;
+    }
+  }
+  
+  showGameOver() {
+    const existing = document.querySelector('.game-over-overlay');
+    if (existing) existing.remove();
+    
+    const overlay = document.createElement('div');
+    overlay.className = 'game-over-overlay';
+    
+    const content = document.createElement('div');
+    content.className = 'game-over-content';
+    
+    const title = document.createElement('h2');
+    if (this.gameState.winner === 'draw') {
+      title.textContent = 'Stalemate!';
+    } else {
+      title.textContent = 'Checkmate!';
+    }
+    
+    const message = document.createElement('p');
+    if (this.gameState.winner === 'draw') {
+      message.textContent = 'The game is a draw.';
+    } else {
+      const winner = this.gameState.winner.charAt(0).toUpperCase() + this.gameState.winner.slice(1);
+      message.textContent = `${winner} wins!`;
+    }
+    
+    const btn = document.createElement('button');
+    btn.textContent = 'New Game';
+    btn.addEventListener('click', () => {
+      window.location.href = this.getBaseUrl();
+    });
+    
+    content.appendChild(title);
+    content.appendChild(message);
+    content.appendChild(btn);
+    overlay.appendChild(content);
+    document.body.appendChild(overlay);
+  }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+  new DistortionChess();
+});
+  </script>
+  <footer style="text-align: center; padding: 20px; margin-top: 20px; opacity: 0.6; font-size: 0.9em;">
+    Created by Claude (Anthropic) with human collaboration, March 2026. Original by <a href="https://www.youtube.com/@ZaneGamesss">ZaneGames</a>.
+  </footer>
+</body>
+</html>
index 410b1f9510ad4a1e4326eec3e2c735c6753f7bd0..6db7ee041fb61062b5c6f3210be8629dc0b64913 100644 (file)
@@ -37,6 +37,9 @@ see also [hack-a-day](/hackaday), my challenge to write one piece of software a
 | [xor](https://github.com/za3k/short-programs#xor)                           | xor two files together. see also [add-base-26](https://github.com/za3k/short-programs#add-base26). | *2020-04-20* | *works*
 | [youtube-autodl](https://github.com/za3k/youtube-autodl)                    | automatically download+organize youtube channels and playlists | *2022-07-07* | *beta*
 
+##ai-written
+| [distortion-chess](https://za3k.com/distortion-chess)                       | play Distortion Chess, a game by [ZaneGames](https://www.youtube.com/@ZaneGamesss) | *2026-03-03* | *beta*
+
 ##less useful software
 | project                                                        | description | written | status
 |--------|---------|---------|-------------|