chess/js/main.js
Christoph Wagner 9011e3b51e
All checks were successful
CI Pipeline / Code Linting (pull_request) Successful in 13s
CI Pipeline / Run Tests (pull_request) Successful in 22s
CI Pipeline / Build Verification (pull_request) Successful in 14s
CI Pipeline / Generate Quality Report (pull_request) Successful in 20s
fix: correct all DOM element ID mismatches and add null safety checks
Fixes critical regression where moves weren't reflected in UI and
turns weren't switching properly.

Root Cause:
- updateTurnIndicator() was looking for 'turn-indicator' but HTML has 'current-turn'
- This caused a null reference error that broke the entire update chain
- Prevented board updates, turn switching, and move history from working

Changes:
1. Fix turn indicator ID: 'turn-indicator' → 'current-turn' (line 175)
2. Add null check for turn indicator to prevent crashes (line 176)
3. Add null check for status-message element (line 239)
4. Add null check for promotion-overlay element (line 266)
5. Add null check for btn-offer-draw element (line 87)

All fixes include graceful degradation with console warnings instead
of throwing errors that break game functionality.

Testing:
- All 124 tests passing 
- ESLint passes with 0 errors (6 pre-existing warnings)
- Move history displays correctly
- Captured pieces display correctly
- Turn indicator updates correctly
- Game flow works as expected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 15:20:06 +01:00

337 lines
9.8 KiB
JavaScript

/**
* main.js - Application entry point
* Initializes game and connects all components
*/
import { GameController } from './controllers/GameController.js';
import { BoardRenderer } from './views/BoardRenderer.js';
import { DragDropHandler } from './controllers/DragDropHandler.js';
class ChessApp {
constructor() {
// Initialize components
this.game = new GameController({
autoSave: true,
enableTimer: false
});
this.renderer = new BoardRenderer(
document.getElementById('chess-board'),
{
showCoordinates: true,
pieceStyle: 'symbols',
highlightLastMove: true
}
);
this.dragDropHandler = new DragDropHandler(this.game, this.renderer);
// Initialize UI
this.initializeUI();
this.setupEventListeners();
this.setupGameEventListeners();
// Start new game
this.game.newGame();
this.updateDisplay();
}
/**
* Initialize UI components
*/
initializeUI() {
// Render initial board
this.renderer.renderBoard(this.game.board, this.game.gameState);
// Setup drag and drop
this.dragDropHandler.setupEventListeners();
// Update status
this.updateTurnIndicator();
}
/**
* Setup button event listeners
*/
setupEventListeners() {
// New Game
document.getElementById('btn-new-game').addEventListener('click', () => {
if (confirm('Start a new game? Current game will be lost.')) {
this.game.newGame();
this.updateDisplay();
this.showMessage('New game started!');
}
});
// Undo
document.getElementById('btn-undo').addEventListener('click', () => {
if (this.game.undo()) {
this.updateDisplay();
this.showMessage('Move undone');
} else {
this.showMessage('Nothing to undo');
}
});
// Redo
document.getElementById('btn-redo').addEventListener('click', () => {
if (this.game.redo()) {
this.updateDisplay();
this.showMessage('Move redone');
} else {
this.showMessage('Nothing to redo');
}
});
// Offer Draw
const offerDrawBtn = document.getElementById('btn-offer-draw');
if (offerDrawBtn) {
offerDrawBtn.addEventListener('click', () => {
this.game.offerDraw();
this.showMessage('Draw offered to opponent');
});
}
// Resign
document.getElementById('btn-resign').addEventListener('click', () => {
if (confirm('Are you sure you want to resign?')) {
this.game.resign();
}
});
}
/**
* Setup game event listeners
*/
setupGameEventListeners() {
// Move made
this.game.on('move', (data) => {
this.updateDisplay();
this.playMoveSound();
});
// Check
this.game.on('check', (data) => {
this.showMessage(`Check! ${data.color} king is in check`);
this.playCheckSound();
});
// Checkmate
this.game.on('checkmate', (data) => {
this.showMessage(`Checkmate! ${data.winner} wins!`, 'success');
this.dragDropHandler.disable();
this.playCheckmateSound();
});
// Stalemate
this.game.on('stalemate', () => {
this.showMessage('Stalemate! Game is a draw', 'info');
this.dragDropHandler.disable();
});
// Draw
this.game.on('draw', (data) => {
this.showMessage(`Draw by ${data.reason}`, 'info');
this.dragDropHandler.disable();
});
// Resign
this.game.on('resign', (data) => {
const winner = data.loser === 'white' ? 'Black' : 'White';
this.showMessage(`${data.loser} resigned. ${winner} wins!`, 'success');
this.dragDropHandler.disable();
});
// Promotion
this.game.on('promotion', (data) => {
this.showPromotionDialog(data.pawn, data.position);
});
// New Game
this.game.on('newgame', () => {
this.dragDropHandler.enable();
this.updateDisplay();
});
}
/**
* Update all display elements
*/
updateDisplay() {
// Re-render board
this.renderer.renderBoard(this.game.board, this.game.gameState);
// Update turn indicator
this.updateTurnIndicator();
// Update move history
this.updateMoveHistory();
// Update captured pieces
this.updateCapturedPieces();
}
/**
* Update turn indicator
*/
updateTurnIndicator() {
const indicator = document.getElementById('current-turn');
if (!indicator) {
console.error('Turn indicator element not found');
return;
}
const turn = this.game.currentTurn;
indicator.textContent = `${turn.charAt(0).toUpperCase() + turn.slice(1)}'s Turn`;
indicator.style.color = turn === 'white' ? '#ffffff' : '#333333';
}
/**
* Update move history display
*/
updateMoveHistory() {
const moveList = document.getElementById('move-history');
const history = this.game.gameState.moveHistory;
if (history.length === 0) {
moveList.innerHTML = '<p style="color: #999; font-style: italic;">No moves yet</p>';
return;
}
let html = '';
for (let i = 0; i < history.length; i += 2) {
const moveNumber = Math.floor(i / 2) + 1;
const whiteMove = history[i];
const blackMove = history[i + 1];
html += `<div>${moveNumber}. ${whiteMove.notation}`;
if (blackMove) {
html += ` ${blackMove.notation}`;
}
html += '</div>';
}
moveList.innerHTML = html;
moveList.scrollTop = moveList.scrollHeight;
}
/**
* Update captured pieces display
*/
updateCapturedPieces() {
const whiteCaptured = document.getElementById('captured-white-pieces');
const blackCaptured = document.getElementById('captured-black-pieces');
const captured = this.game.gameState.capturedPieces;
whiteCaptured.innerHTML = captured.black.map(piece =>
`<span class="captured-piece black">${piece.getSymbol()}</span>`
).join('') || '-';
blackCaptured.innerHTML = captured.white.map(piece =>
`<span class="captured-piece white">${piece.getSymbol()}</span>`
).join('') || '-';
}
/**
* Show message to user
* @param {string} message - Message text
* @param {string} type - Message type (info, success, error)
*/
showMessage(message, type = 'info') {
const statusMessage = document.getElementById('status-message');
if (!statusMessage) {
console.warn('Status message element not found, using console:', message);
return;
}
statusMessage.textContent = message;
statusMessage.style.display = 'block';
// Auto-hide after 3 seconds
setTimeout(() => {
statusMessage.style.display = 'none';
}, 3000);
}
/**
* Show promotion dialog
* @param {Pawn} pawn - Pawn to promote
* @param {Position} position - Pawn position
*/
showPromotionDialog(pawn, position) {
const overlay = document.getElementById('promotion-overlay');
const dialog = document.getElementById('promotion-dialog');
if (!dialog) {
console.error('Promotion dialog not found');
return;
}
if (overlay) {
overlay.style.display = 'block';
}
dialog.style.display = 'block';
// Update symbols for current color
const symbols = pawn.color === 'white' ?
{ queen: '♕', rook: '♖', bishop: '♗', knight: '♘' } :
{ queen: '♛', rook: '♜', bishop: '♝', knight: '♞' };
document.querySelectorAll('.promotion-piece .symbol').forEach(el => {
const type = el.parentElement.dataset.type;
el.textContent = symbols[type];
el.style.color = pawn.color === 'white' ? '#ffffff' : '#000000';
});
// Handle selection
const handleSelection = (e) => {
const pieceType = e.currentTarget.dataset.type;
// Promote pawn
import('./engine/SpecialMoves.js').then(({ SpecialMoves }) => {
SpecialMoves.promote(this.game.board, pawn, pieceType);
this.updateDisplay();
});
// Hide dialog
overlay.style.display = 'none';
dialog.style.display = 'none';
// Remove listeners
document.querySelectorAll('.promotion-piece').forEach(el => {
el.removeEventListener('click', handleSelection);
});
};
document.querySelectorAll('.promotion-piece').forEach(el => {
el.addEventListener('click', handleSelection);
});
}
/**
* Play move sound (optional - can be implemented)
*/
playMoveSound() {
// TODO: Add sound effect
}
/**
* Play check sound (optional - can be implemented)
*/
playCheckSound() {
// TODO: Add sound effect
}
/**
* Play checkmate sound (optional - can be implemented)
*/
playCheckmateSound() {
// TODO: Add sound effect
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.chessApp = new ChessApp();
console.log('Chess game initialized successfully!');
});