comparativos

Claude Haiku 4.5 · Claude Code Web — Jogo de xadrez 3D em um unico HTML

~/testes/xadrez/claude-haiku-4-5-claude-code-web  — Jogo de xadrez 3D em um unico HTML

abrir em nova aba
Claude Haiku 4.5 Anthropic Claude Code Web
  • Leonardo
  • ~2min
carregando artefato

src/index.html

html955 linhas30 KB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Xadrez 3D</title>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
    }
  }
  </script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Arial', sans-serif;
      background: #1a1a1a;
      color: #e0e0e0;
      height: 100vh;
      overflow: hidden;
      display: flex;
    }

    #canvas {
      flex: 1;
      display: block;
    }

    #panel {
      width: 300px;
      background: #2a2a2a;
      border-left: 1px solid #444;
      display: flex;
      flex-direction: column;
      padding: 20px;
      overflow-y: auto;
      font-size: 13px;
    }

    .panel-section {
      margin-bottom: 20px;
      border-bottom: 1px solid #444;
      padding-bottom: 15px;
    }

    .panel-section:last-child {
      border-bottom: none;
    }

    .panel-title {
      font-weight: bold;
      font-size: 14px;
      margin-bottom: 10px;
      color: #ffd700;
    }

    #turnInfo {
      font-size: 16px;
      font-weight: bold;
      color: #fff;
      padding: 10px;
      border-radius: 4px;
      text-align: center;
      margin-bottom: 10px;
    }

    .white-turn {
      background: #fff;
      color: #000;
    }

    .black-turn {
      background: #333;
      color: #fff;
      border: 2px solid #ffd700;
    }

    #gameStatus {
      background: #3a3a3a;
      padding: 8px;
      border-radius: 4px;
      color: #90ee90;
    }

    #moveHistory {
      max-height: 150px;
      overflow-y: auto;
    }

    .move-item {
      padding: 4px 0;
      border-bottom: 1px solid #333;
      font-family: monospace;
      font-size: 12px;
    }

    .move-item:last-child {
      border-bottom: none;
    }

    #capturedPieces {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 5px;
    }

    .captured-piece {
      background: #3a3a3a;
      padding: 5px;
      border-radius: 3px;
      text-align: center;
      font-size: 12px;
    }

    .info-box {
      background: #3a3a3a;
      padding: 8px;
      border-radius: 4px;
      font-size: 12px;
      color: #aaa;
      margin-top: 10px;
    }

    button {
      background: #ffd700;
      color: #000;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      font-weight: bold;
      font-size: 12px;
      margin-top: 10px;
      width: 100%;
      transition: background 0.3s;
    }

    button:hover {
      background: #ffed4e;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <div id="panel">
    <div id="turnInfo" class="white-turn">BRANCO</div>

    <div class="panel-section">
      <div class="panel-title">Status</div>
      <div id="gameStatus">Jogo em andamento</div>
    </div>

    <div class="panel-section">
      <div class="panel-title">Histórico de Lances</div>
      <div id="moveHistory"></div>
    </div>

    <div class="panel-section">
      <div class="panel-title">Peças Capturadas</div>
      <div id="capturedPieces"></div>
      <div class="info-box">
        <strong>Controles:</strong><br>
        • Clique: selecionar peça<br>
        • Arrastar: orbitar câmera<br>
        • Scroll: zoom
      </div>
    </div>

    <button onclick="location.reload()">Novo Jogo</button>
  </div>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    // ==================== SETUP INICIAL ====================
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x1a1a1a);

    const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 12, 12);
    camera.lookAt(0, 0, 0);

    const canvas = document.getElementById('canvas');
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
    renderer.setSize(window.innerWidth - 300, window.innerHeight);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFShadowShadowMap;

    // Controles orbitais
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 0, 0);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.autoRotate = false;

    // Iluminação
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(5, 15, 10);
    directionalLight.castShadow = true;
    directionalLight.shadow.mapSize.width = 2048;
    directionalLight.shadow.mapSize.height = 2048;
    directionalLight.shadow.camera.near = 0.5;
    directionalLight.shadow.camera.far = 50;
    directionalLight.shadow.camera.left = -20;
    directionalLight.shadow.camera.right = 20;
    directionalLight.shadow.camera.top = 20;
    directionalLight.shadow.camera.bottom = -20;
    scene.add(directionalLight);

    // ==================== LÓGICA DE XADREZ ====================
    const PIECES = {
      K: 'king', Q: 'queen', R: 'rook', B: 'bishop', N: 'knight', P: 'pawn',
      k: 'king', q: 'queen', r: 'rook', b: 'bishop', n: 'knight', p: 'pawn'
    };

    class ChessGame {
      constructor() {
        this.board = [
          ['r','n','b','q','k','b','n','r'],
          ['p','p','p','p','p','p','p','p'],
          [null,null,null,null,null,null,null,null],
          [null,null,null,null,null,null,null,null],
          [null,null,null,null,null,null,null,null],
          [null,null,null,null,null,null,null,null],
          ['P','P','P','P','P','P','P','P'],
          ['R','N','B','Q','K','B','N','R']
        ];
        this.isWhiteTurn = true;
        this.moveHistory = [];
        this.capturedPieces = [];
        this.selectedSquare = null;
        this.enPassantSquare = null;
        this.castlingRights = {
          whiteKingside: true, whiteQueenside: true,
          blackKingside: true, blackQueenside: true
        };
        this.halfmoveClock = 0;
        this.fullmoveNumber = 1;
      }

      getPieceColor(piece) {
        return piece === piece.toUpperCase() ? 'white' : 'black';
      }

      isLegalMove(fromRow, fromCol, toRow, toCol) {
        const piece = this.board[fromRow][fromCol];
        if (!piece) return false;

        const color = this.getPieceColor(piece);
        if (this.isWhiteTurn !== (color === 'white')) return false;

        const target = this.board[toRow][toCol];
        if (target && this.getPieceColor(target) === color) return false;

        const pieceName = piece.toUpperCase();
        let isMoveLegal = false;

        if (pieceName === 'P') {
          isMoveLegal = this.isLegalPawnMove(fromRow, fromCol, toRow, toCol);
        } else if (pieceName === 'N') {
          isMoveLegal = this.isLegalKnightMove(fromRow, fromCol, toRow, toCol);
        } else if (pieceName === 'B') {
          isMoveLegal = this.isLegalBishopMove(fromRow, fromCol, toRow, toCol);
        } else if (pieceName === 'R') {
          isMoveLegal = this.isLegalRookMove(fromRow, fromCol, toRow, toCol);
        } else if (pieceName === 'Q') {
          isMoveLegal = this.isLegalQueenMove(fromRow, fromCol, toRow, toCol);
        } else if (pieceName === 'K') {
          isMoveLegal = this.isLegalKingMove(fromRow, fromCol, toRow, toCol);
        }

        if (!isMoveLegal) return false;

        // Verificar se deixa o rei em xeque
        const testBoard = JSON.parse(JSON.stringify(this.board));
        testBoard[toRow][toCol] = testBoard[fromRow][fromCol];
        testBoard[fromRow][fromCol] = null;

        const testGame = Object.create(this);
        testGame.board = testBoard;

        return !testGame.isInCheck(color === 'white');
      }

      isLegalPawnMove(fromRow, fromCol, toRow, toCol) {
        const piece = this.board[fromRow][fromCol];
        const isWhite = piece === piece.toUpperCase();
        const direction = isWhite ? -1 : 1;
        const startRow = isWhite ? 6 : 1;

        const rowDiff = toRow - fromRow;
        const colDiff = Math.abs(toCol - fromCol);
        const target = this.board[toRow][toCol];

        // Movimento para frente
        if (toCol === fromCol && target === null) {
          if (rowDiff === direction) return true;
          if (fromRow === startRow && rowDiff === 2 * direction && this.board[fromRow + direction][fromCol] === null) {
            return true;
          }
        }

        // Captura diagonal
        if (colDiff === 1 && rowDiff === direction) {
          if (target !== null) return true;
          // En passant
          if (this.enPassantSquare && toRow === this.enPassantSquare[0] && toCol === this.enPassantSquare[1]) {
            return true;
          }
        }

        return false;
      }

      isLegalKnightMove(fromRow, fromCol, toRow, toCol) {
        const rowDiff = Math.abs(toRow - fromRow);
        const colDiff = Math.abs(toCol - fromCol);
        return (rowDiff === 2 && colDiff === 1) || (rowDiff === 1 && colDiff === 2);
      }

      isPathClear(fromRow, fromCol, toRow, toCol) {
        const rowDir = Math.sign(toRow - fromRow);
        const colDir = Math.sign(toCol - fromCol);
        let r = fromRow + rowDir;
        let c = fromCol + colDir;

        while (r !== toRow || c !== toCol) {
          if (this.board[r][c] !== null) return false;
          r += rowDir;
          c += colDir;
        }
        return true;
      }

      isLegalBishopMove(fromRow, fromCol, toRow, toCol) {
        const rowDiff = Math.abs(toRow - fromRow);
        const colDiff = Math.abs(toCol - fromCol);
        return rowDiff === colDiff && rowDiff > 0 && this.isPathClear(fromRow, fromCol, toRow, toCol);
      }

      isLegalRookMove(fromRow, fromCol, toRow, toCol) {
        return ((fromRow === toRow || fromCol === toCol) &&
                !(fromRow === toRow && fromCol === toCol) &&
                this.isPathClear(fromRow, fromCol, toRow, toCol));
      }

      isLegalQueenMove(fromRow, fromCol, toRow, toCol) {
        return this.isLegalBishopMove(fromRow, fromCol, toRow, toCol) ||
               this.isLegalRookMove(fromRow, fromCol, toRow, toCol);
      }

      isLegalKingMove(fromRow, fromCol, toRow, toCol) {
        const rowDiff = Math.abs(toRow - fromRow);
        const colDiff = Math.abs(toCol - fromCol);

        // Movimento normal do rei
        if (rowDiff <= 1 && colDiff <= 1 && (rowDiff + colDiff > 0)) {
          return true;
        }

        // Roque
        if (fromRow === toRow && colDiff === 2 && (fromRow === 0 || fromRow === 7)) {
          return this.isLegalCastle(fromRow, fromCol, toRow, toCol);
        }

        return false;
      }

      isLegalCastle(kingRow, kingCol, toRow, rookCol) {
        const isWhite = kingRow === 7;

        if (!this.isPathClear(kingRow, kingCol, toRow, rookCol)) return false;

        const rookStartCol = toCol > kingCol ? 7 : 0;
        if (this.board[kingRow][rookStartCol] !== (isWhite ? 'R' : 'r')) return false;

        if (isWhite) {
          return toCol > kingCol ? this.castlingRights.whiteKingside : this.castlingRights.whiteQueenside;
        } else {
          return toCol > kingCol ? this.castlingRights.blackKingside : this.castlingRights.blackQueenside;
        }
      }

      findKing(isWhite) {
        const king = isWhite ? 'K' : 'k';
        for (let r = 0; r < 8; r++) {
          for (let c = 0; c < 8; c++) {
            if (this.board[r][c] === king) return [r, c];
          }
        }
        return null;
      }

      isInCheck(isWhite) {
        const kingPos = this.findKing(isWhite);
        if (!kingPos) return false;

        const [kRow, kCol] = kingPos;

        for (let r = 0; r < 8; r++) {
          for (let c = 0; c < 8; c++) {
            const piece = this.board[r][c];
            if (piece && this.getPieceColor(piece) !== (isWhite ? 'white' : 'black')) {
              if (this.canPieceAttack(r, c, kRow, kCol)) {
                return true;
              }
            }
          }
        }
        return false;
      }

      canPieceAttack(fromRow, fromCol, toRow, toCol) {
        const piece = this.board[fromRow][fromCol];
        if (!piece) return false;

        const pieceName = piece.toUpperCase();

        if (pieceName === 'P') {
          const isWhite = piece === 'P';
          const direction = isWhite ? -1 : 1;
          const colDiff = Math.abs(toCol - fromCol);
          return toRow - fromRow === direction && colDiff === 1;
        }

        if (pieceName === 'N') {
          return this.isLegalKnightMove(fromRow, fromCol, toRow, toCol);
        }

        if (pieceName === 'B') {
          const rowDiff = Math.abs(toRow - fromRow);
          const colDiff = Math.abs(toCol - fromCol);
          return rowDiff === colDiff && rowDiff > 0 && this.isPathClear(fromRow, fromCol, toRow, toCol);
        }

        if (pieceName === 'R') {
          return ((fromRow === toRow || fromCol === toCol) &&
                  !(fromRow === toRow && fromCol === toCol) &&
                  this.isPathClear(fromRow, fromCol, toRow, toCol));
        }

        if (pieceName === 'Q') {
          const rowDiff = Math.abs(toRow - fromRow);
          const colDiff = Math.abs(toCol - fromCol);
          if (rowDiff === colDiff || fromRow === toRow || fromCol === toCol) {
            if (rowDiff + colDiff > 0) return this.isPathClear(fromRow, fromCol, toRow, toCol);
          }
        }

        if (pieceName === 'K') {
          const rowDiff = Math.abs(toRow - fromRow);
          const colDiff = Math.abs(toCol - fromCol);
          return rowDiff <= 1 && colDiff <= 1 && (rowDiff + colDiff > 0);
        }

        return false;
      }

      hasLegalMoves(isWhite) {
        for (let r = 0; r < 8; r++) {
          for (let c = 0; c < 8; c++) {
            const piece = this.board[r][c];
            if (piece && this.getPieceColor(piece) === (isWhite ? 'white' : 'black')) {
              for (let tr = 0; tr < 8; tr++) {
                for (let tc = 0; tc < 8; tc++) {
                  if (this.isLegalMove(r, c, tr, tc)) {
                    return true;
                  }
                }
              }
            }
          }
        }
        return false;
      }

      movePiece(fromRow, fromCol, toRow, toCol) {
        if (!this.isLegalMove(fromRow, fromCol, toRow, toCol)) {
          return false;
        }

        const piece = this.board[fromRow][fromCol];
        const target = this.board[toRow][toCol];
        const pieceName = piece.toUpperCase();

        // Captura regular
        if (target) {
          this.capturedPieces.push(target);
        }

        // En passant
        if (pieceName === 'P' && fromCol !== toCol && target === null) {
          const captureRow = fromRow;
          const captured = this.board[captureRow][toCol];
          if (captured) this.capturedPieces.push(captured);
          this.board[captureRow][toCol] = null;
        }

        // Roque
        if (pieceName === 'K' && Math.abs(toCol - fromCol) === 2) {
          const rookCol = toCol > fromCol ? 7 : 0;
          const newRookCol = toCol > fromCol ? 5 : 3;
          this.board[fromRow][newRookCol] = this.board[fromRow][rookCol];
          this.board[fromRow][rookCol] = null;
        }

        // Promoção de peão
        if (pieceName === 'P' && (toRow === 0 || toRow === 7)) {
          this.board[toRow][toCol] = piece.toUpperCase() === piece ? 'Q' : 'q';
        } else {
          this.board[toRow][toCol] = piece;
        }

        this.board[fromRow][fromCol] = null;

        // Atualizar direitos de roque
        if (pieceName === 'K') {
          if (piece === 'K') {
            this.castlingRights.whiteKingside = false;
            this.castlingRights.whiteQueenside = false;
          } else {
            this.castlingRights.blackKingside = false;
            this.castlingRights.blackQueenside = false;
          }
        }
        if (pieceName === 'R') {
          if (fromRow === 7) {
            if (fromCol === 0) this.castlingRights.whiteQueenside = false;
            if (fromCol === 7) this.castlingRights.whiteKingside = false;
          }
          if (fromRow === 0) {
            if (fromCol === 0) this.castlingRights.blackQueenside = false;
            if (fromCol === 7) this.castlingRights.blackKingside = false;
          }
        }

        // En passant
        if (pieceName === 'P' && Math.abs(toRow - fromRow) === 2) {
          this.enPassantSquare = [fromRow + (toRow - fromRow) / 2, fromCol];
        } else {
          this.enPassantSquare = null;
        }

        // Registro de movimento
        const moveNotation = String.fromCharCode(97 + fromCol) + (8 - fromRow) + '-' +
                             String.fromCharCode(97 + toCol) + (8 - toRow);
        this.moveHistory.push(moveNotation);

        this.isWhiteTurn = !this.isWhiteTurn;

        return true;
      }

      getGameStatus() {
        const isWhite = this.isWhiteTurn;
        const hasLegal = this.hasLegalMoves(isWhite);

        if (!hasLegal) {
          if (this.isInCheck(isWhite)) {
            return isWhite ? 'xeque-mate (preto venceu)' : 'xeque-mate (branco venceu)';
          }
          return 'empate por afogamento';
        }

        if (this.isInCheck(isWhite)) {
          return isWhite ? 'xeque (branco)' : 'xeque (preto)';
        }

        return 'jogo em andamento';
      }

      getLegalSquares(row, col) {
        const squares = [];
        for (let r = 0; r < 8; r++) {
          for (let c = 0; c < 8; c++) {
            if (this.isLegalMove(row, col, r, c)) {
              squares.push([r, c]);
            }
          }
        }
        return squares;
      }
    }

    const game = new ChessGame();
    let selectedPiece = null;
    let legalSquares = [];

    // ==================== RENDERIZAÇÃO 3D ====================
    const BOARD_SIZE = 8;
    const SQUARE_SIZE = 1;
    const BOARD_OFFSET = (BOARD_SIZE * SQUARE_SIZE) / 2;

    function createBoard() {
      const group = new THREE.Group();

      for (let row = 0; row < BOARD_SIZE; row++) {
        for (let col = 0; col < BOARD_SIZE; col++) {
          const isDark = (row + col) % 2 === 1;
          const color = isDark ? 0x654321 : 0xe8d4b8;

          const geometry = new THREE.PlaneGeometry(SQUARE_SIZE, SQUARE_SIZE);
          const material = new THREE.MeshLambertMaterial({ color });
          const square = new THREE.Mesh(geometry, material);

          square.position.set(
            col * SQUARE_SIZE - BOARD_OFFSET + SQUARE_SIZE / 2,
            0,
            row * SQUARE_SIZE - BOARD_OFFSET + SQUARE_SIZE / 2
          );
          square.rotation.x = -Math.PI / 2;
          square.receiveShadow = true;
          square.userData = { row, col, isDark };

          group.add(square);
        }
      }

      return group;
    }

    function createPiece(name, color) {
      const group = new THREE.Group();

      if (name === 'pawn') {
        // Corpo do peão
        const body = new THREE.CylinderGeometry(0.25, 0.3, 0.4, 16);
        const mesh = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color }));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        group.add(mesh);

        // Topo
        const top = new THREE.SphereGeometry(0.25, 16, 16);
        const topMesh = new THREE.Mesh(top, new THREE.MeshStandardMaterial({ color }));
        topMesh.position.y = 0.25;
        topMesh.castShadow = true;
        topMesh.receiveShadow = true;
        group.add(topMesh);
      } else if (name === 'knight') {
        // Cavalo simplificado
        const body = new THREE.BoxGeometry(0.3, 0.5, 0.5);
        const mesh = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color }));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        group.add(mesh);

        const head = new THREE.SphereGeometry(0.2, 16, 16);
        const headMesh = new THREE.Mesh(head, new THREE.MeshStandardMaterial({ color }));
        headMesh.position.y = 0.35;
        headMesh.position.z = 0.2;
        headMesh.castShadow = true;
        headMesh.receiveShadow = true;
        group.add(headMesh);
      } else if (name === 'bishop') {
        // Bispo
        const body = new THREE.CylinderGeometry(0.25, 0.35, 0.6, 16);
        const mesh = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color }));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        group.add(mesh);

        const top = new THREE.ConeGeometry(0.2, 0.4, 16);
        const topMesh = new THREE.Mesh(top, new THREE.MeshStandardMaterial({ color }));
        topMesh.position.y = 0.5;
        topMesh.castShadow = true;
        topMesh.receiveShadow = true;
        group.add(topMesh);
      } else if (name === 'rook') {
        // Torre
        const body = new THREE.BoxGeometry(0.35, 0.6, 0.35);
        const mesh = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color }));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        group.add(mesh);

        // Parte superior
        const top = new THREE.BoxGeometry(0.4, 0.2, 0.4);
        const topMesh = new THREE.Mesh(top, new THREE.MeshStandardMaterial({ color }));
        topMesh.position.y = 0.4;
        topMesh.castShadow = true;
        topMesh.receiveShadow = true;
        group.add(topMesh);
      } else if (name === 'queen') {
        // Rainha
        const body = new THREE.CylinderGeometry(0.3, 0.35, 0.6, 16);
        const mesh = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color }));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        group.add(mesh);

        const top = new THREE.SphereGeometry(0.28, 16, 16);
        const topMesh = new THREE.Mesh(top, new THREE.MeshStandardMaterial({ color }));
        topMesh.position.y = 0.45;
        topMesh.castShadow = true;
        topMesh.receiveShadow = true;
        group.add(topMesh);

        // Coroa
        const crown = new THREE.ConeGeometry(0.35, 0.3, 8);
        const crownMesh = new THREE.Mesh(crown, new THREE.MeshStandardMaterial({ color }));
        crownMesh.position.y = 0.65;
        crownMesh.castShadow = true;
        crownMesh.receiveShadow = true;
        group.add(crownMesh);
      } else if (name === 'king') {
        // Rei
        const body = new THREE.CylinderGeometry(0.3, 0.35, 0.6, 16);
        const mesh = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color }));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        group.add(mesh);

        const top = new THREE.SphereGeometry(0.3, 16, 16);
        const topMesh = new THREE.Mesh(top, new THREE.MeshStandardMaterial({ color }));
        topMesh.position.y = 0.45;
        topMesh.castShadow = true;
        topMesh.receiveShadow = true;
        group.add(topMesh);

        // Cruz no topo
        const cross = new THREE.CylinderGeometry(0.05, 0.05, 0.3, 8);
        const crossMesh = new THREE.Mesh(cross, new THREE.MeshStandardMaterial({ color }));
        crossMesh.position.y = 0.75;
        crossMesh.castShadow = true;
        crossMesh.receiveShadow = true;
        group.add(crossMesh);
      }

      return group;
    }

    const boardGroup = createBoard();
    scene.add(boardGroup);

    const pieces = {};

    function initializePieces() {
      const whiteMaterial = new THREE.MeshStandardMaterial({
        color: 0xf5f5dc,
        metalness: 0.3,
        roughness: 0.4
      });

      const blackMaterial = new THREE.MeshStandardMaterial({
        color: 0x1a1a1a,
        metalness: 0.3,
        roughness: 0.4
      });

      for (let row = 0; row < 8; row++) {
        for (let col = 0; col < 8; col++) {
          const piece = game.board[row][col];
          if (piece) {
            const pieceName = PIECES[piece];
            const isWhite = piece === piece.toUpperCase();
            const color = isWhite ? 0xf5f5dc : 0x2a2a2a;

            const mesh = createPiece(pieceName, color);
            mesh.position.set(
              col * SQUARE_SIZE - BOARD_OFFSET + SQUARE_SIZE / 2,
              0.3,
              row * SQUARE_SIZE - BOARD_OFFSET + SQUARE_SIZE / 2
            );

            mesh.userData = { row, col, piece };
            scene.add(mesh);

            pieces[row + ',' + col] = mesh;
          }
        }
      }
    }

    initializePieces();

    // ==================== INTERAÇÃO ====================
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    const highlightedSquares = [];

    function updateHighlight() {
      highlightedSquares.forEach(square => {
        square.material.emissive.setHex(0x000000);
      });
      highlightedSquares.length = 0;

      if (selectedPiece) {
        const [row, col] = selectedPiece;
        legalSquares = game.getLegalSquares(row, col);

        legalSquares.forEach(([r, c]) => {
          const worldCol = c;
          const worldRow = r;
          for (const square of boardGroup.children) {
            if (square.userData.row === worldRow && square.userData.col === worldCol) {
              square.material.emissive.setHex(0x4c9f00);
              highlightedSquares.push(square);
            }
          }
        });

        // Destacar peça selecionada
        for (const square of boardGroup.children) {
          if (square.userData.row === row && square.userData.col === col) {
            square.material.emissive.setHex(0xffd700);
            highlightedSquares.push(square);
          }
        }
      }
    }

    function onMouseClick(event) {
      mouse.x = (event.clientX / (window.innerWidth - 300)) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

      raycaster.setFromCamera(mouse, camera);

      const pieceObjects = Object.values(pieces);
      const intersects = raycaster.intersectObjects(pieceObjects, true);

      if (intersects.length > 0) {
        const clickedPiece = intersects[0].object.parent;
        const { row, col } = clickedPiece.userData;

        if (selectedPiece && selectedPiece[0] === row && selectedPiece[1] === col) {
          selectedPiece = null;
        } else if (game.board[row][col] && game.getPieceColor(game.board[row][col]) === (game.isWhiteTurn ? 'white' : 'black')) {
          selectedPiece = [row, col];
        }

        updateHighlight();
        return;
      }

      const boardIntersects = raycaster.intersectObjects(boardGroup.children);
      if (boardIntersects.length > 0 && selectedPiece) {
        const clickedSquare = boardIntersects[0].object;
        const targetRow = clickedSquare.userData.row;
        const targetCol = clickedSquare.userData.col;

        if (game.movePiece(selectedPiece[0], selectedPiece[1], targetRow, targetCol)) {
          // Mover peça 3D
          const pieceKey = selectedPiece[0] + ',' + selectedPiece[1];
          const pieceMesh = pieces[pieceKey];

          if (pieceMesh) {
            delete pieces[pieceKey];
            pieceMesh.position.set(
              targetCol * SQUARE_SIZE - BOARD_OFFSET + SQUARE_SIZE / 2,
              0.3,
              targetRow * SQUARE_SIZE - BOARD_OFFSET + SQUARE_SIZE / 2
            );
            pieceMesh.userData = { row: targetRow, col: targetCol, piece: game.board[targetRow][targetCol] };
            pieces[targetRow + ',' + targetCol] = pieceMesh;
          }

          // Remover peças capturadas
          const oldTarget = boardIntersects[0].object.userData;
          for (let r = 0; r < 8; r++) {
            for (let c = 0; c < 8; c++) {
              const key = r + ',' + c;
              if (key !== targetRow + ',' + targetCol && !game.board[r][c] && pieces[key]) {
                scene.remove(pieces[key]);
                delete pieces[key];
              }
            }
          }
        }

        selectedPiece = null;
        updateHighlight();
        updateUI();
      }
    }

    renderer.domElement.addEventListener('click', onMouseClick);

    // ==================== UI ====================
    function updateUI() {
      const turnInfo = document.getElementById('turnInfo');
      const gameStatus = document.getElementById('gameStatus');
      const moveHistory = document.getElementById('moveHistory');
      const capturedPieces = document.getElementById('capturedPieces');

      if (game.isWhiteTurn) {
        turnInfo.textContent = 'BRANCO';
        turnInfo.className = 'white-turn';
      } else {
        turnInfo.textContent = 'PRETO';
        turnInfo.className = 'black-turn';
      }

      gameStatus.textContent = game.getGameStatus();

      moveHistory.innerHTML = '';
      game.moveHistory.forEach((move, idx) => {
        const div = document.createElement('div');
        div.className = 'move-item';
        div.textContent = (Math.floor(idx / 2) + 1) + (idx % 2 === 0 ? '. ' : '... ') + move;
        moveHistory.appendChild(div);
      });

      capturedPieces.innerHTML = '';
      const pieceCounts = {};
      const pieceNames = {
        'p': '♟', 'n': '♞', 'b': '♝', 'r': '♜', 'q': '♛', 'k': '♚',
        'P': '♟', 'N': '♞', 'B': '♝', 'R': '♜', 'Q': '♛', 'K': '♚'
      };

      game.capturedPieces.forEach(piece => {
        pieceCounts[piece] = (pieceCounts[piece] || 0) + 1;
      });

      Object.keys(pieceCounts).sort().forEach(piece => {
        const div = document.createElement('div');
        div.className = 'captured-piece';
        div.textContent = pieceNames[piece] + ' x' + pieceCounts[piece];
        capturedPieces.appendChild(div);
      });
    }

    updateUI();

    // ==================== RENDER LOOP ====================
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }

    animate();

    // ==================== RESPONSIVIDADE ====================
    window.addEventListener('resize', () => {
      const width = window.innerWidth - 300;
      const height = window.innerHeight;
      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height);
    });
  </script>
</body>
</html>
claude-haiku-4-5-claude-code-web 1 arquivo 956 linhas 30 KB atalhos p aba preview c aba codigo [ arquivo anterior ] proximo arquivo esc sair do artefato

prompt desta execucao

Crie um jogo de xadrez 3D jogavel em um unico arquivo HTML.

Requisitos funcionais:
- Tabuleiro 8x8 e as 32 pecas renderizadas em 3D com Three.js
- Regras completas: movimentos legais de cada peca, roque, en passant, promocao de peao, xeque, xeque-mate e empate por afogamento
- Dois jogadores no mesmo dispositivo: clicar na peca destaca as casas legais, clicar na casa de destino executa o lance
- Camera orbital com o mouse (arrastar para girar, scroll para zoom)
- Painel com o turno atual, historico de lances e pecas capturadas

Restricoes tecnicas (obrigatorias):
- UM unico arquivo index.html. Todo o CSS e todo o JavaScript inline. Nao crie nenhum outro arquivo
- Three.js vem do CDN, declarado num <script type="importmap"> inline e consumido por um <script type="module"> tambem inline:

  <script type="importmap">
  {"imports":{
    "three":"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
    "three/addons/":"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
  }}
  </script>
  <script type="module">
  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  </script>

  Atencao: importar https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js pela URL direta NAO funciona — esse arquivo faz `import ... from 'three'`, e o especificador bare so resolve pelo importmap
- NAO use <script type="module" src="..."> apontando para arquivo local
- NAO use localStorage nem sessionStorage — eles lancam SecurityError no ambiente onde a pagina roda
- A pagina roda dentro de um iframe sandbox de origem opaca: sem cookies, sem acesso a janela pai
- Deve caber e ficar legivel em 1440x900
notas do autor
Arquivo unico de 30 KB / 955 linhas, gerado em cerca de um quinto do tempo das outras duas execucoes. O motor de regras esta la e e real: roque com atualizacao dos direitos, en passant, promocao (automatica para dama, sem dialogo de escolha) e deteccao de xeque-mate e de empate por afogamento. Verificado dentro do iframe de origem opaca: carrega sem erro de console e o lance e aplicado — o historico registra "1. e2-e3", em notacao de coordenada, e o turno passa para as pretas. O que separa esta execucao das outras duas e o enquadramento, nao a logica: o tabuleiro sai pequeno e deslocado para a esquerda, sobra uma faixa larga de fundo vazio a direita, e o topo do painel abre com uma caixa branca sem conteudo.