chess/js/main.js
Christoph Wagner e88e67de4b
Some checks failed
CI Pipeline / Code Linting (pull_request) Successful in 13s
CI Pipeline / Run Tests (pull_request) Failing after 21s
CI Pipeline / Build Verification (pull_request) Has been skipped
CI Pipeline / Generate Quality Report (pull_request) Successful in 22s
fix: resolve promotion dialog bugs and column resizing issues
This commit addresses three critical bugs reported after initial PR:

1. **Promotion dialog not closing after piece selection**
   - Changed from `style.display` to HTML5 `.showModal()` and `.close()`
   - Fixed selector from `.promotion-piece .symbol` to `.promotion-piece .piece-icon`
   - Fixed data attribute from `dataset.type` to `dataset.piece`
   - Dialog now properly closes after user selects promotion piece

2. **Pawn showing as queen before user selection**
   - Removed automatic promotion to queen in GameController.js:112-115
   - Now emits 'promotion' event WITHOUT pre-promoting
   - User sees pawn until they select the promotion piece
   - Promotion happens only after user makes their choice

3. **Column resizing not fully fixed**
   - Added explicit `max-width: 250px` to `.game-sidebar` and `.captured-pieces`
   - Added explicit `max-width: 250px` to `.move-history-section`
   - Added `overflow: hidden` to `.captured-list` and `overflow-x: hidden` to `.move-history`
   - Added `min-width: 600px` to `.board-section`
   - Added `width: 100%` to all sidebar components for proper constraint application
   - Columns now maintain stable widths even with content changes

**Files Changed:**
- `js/main.js` - Fixed promotion dialog handling
- `js/controllers/GameController.js` - Removed auto-promotion
- `css/main.css` - Added width constraints and overflow handling

**Root Causes:**
- Dialog: Mixing HTML5 dialog API with legacy display styles
- Promotion: Auto-promoting before showing user dialog
- Resizing: Missing explicit max-widths allowed flex items to grow with content

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 19:28:45 +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;
// "Captured by Black" shows white pieces that black captured
whiteCaptured.innerHTML = captured.white.map(piece =>
`<span class="captured-piece white">${piece.getSymbol()}</span>`
).join('') || '-';
// "Captured by White" shows black pieces that white captured
blackCaptured.innerHTML = captured.black.map(piece =>
`<span class="captured-piece black">${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;
}
// Add type class for styling
statusMessage.className = `status-message ${type}`;
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 dialog = document.getElementById('promotion-dialog');
if (!dialog) {
console.error('Promotion dialog not found');
return;
}
// Show dialog using HTML5 dialog element API
dialog.showModal();
// Update symbols for current color
const symbols = pawn.color === 'white' ?
{ queen: '♕', rook: '♖', bishop: '♗', knight: '♘' } :
{ queen: '♛', rook: '♜', bishop: '♝', knight: '♞' };
document.querySelectorAll('.promotion-piece .piece-icon').forEach(el => {
const type = el.parentElement.dataset.piece;
el.textContent = symbols[type];
});
// Handle selection
const handleSelection = (e) => {
const pieceType = e.currentTarget.dataset.piece;
// Promote pawn
import('./engine/SpecialMoves.js').then(({ SpecialMoves }) => {
SpecialMoves.promote(this.game.board, pawn, pieceType);
this.updateDisplay();
});
// Close dialog using HTML5 dialog element API
dialog.close();
// 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!');
});