Claude Haiku 4.5 · Claude Code Web — 3D chess game in a single HTML file
src/index.html
<!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>no file open
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- A single 30 KB / 955-line file, produced in roughly a fifth of the time the other two runs took. The rules engine is there and it is real: castling with rights tracking, en passant, promotion (auto-queen, no choice dialog) and detection of both checkmate and stalemate. Verified inside the opaque-origin iframe: it loads with no console errors and the move is applied — the history logs "1. e2-e3" in coordinate notation and the turn passes to Black. What sets this run apart from the other two is framing, not logic: the board renders small and pushed to the left, a wide band of empty background is left on the right, and the panel opens with an empty white box at the top.