-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscripts.js
More file actions
2683 lines (2364 loc) · 117 KB
/
scripts.js
File metadata and controls
2683 lines (2364 loc) · 117 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// --- START OF FILE scripts-v1.js ---
// Ensure chess.js is loaded
if (typeof Chess === 'undefined') {
alert("FATAL ERROR: chess.js library not found. Please include it in your HTML.");
throw new Error("chess.js library not found.");
}
const chessboard = document.getElementById('chessboard');
const pieces = { // For rendering (chess.js uses different format internally)
'r': '♜', 'n': '♞', 'b': '♝', 'q': '♛', 'k': '♚', 'p': '♟', // black ascii
'R': '♖', 'N': '♘', 'B': '♗', 'Q': '♕', 'K': '♔', 'P': '♙' // white ascii
};
const pieceValues = { 'p': 1, 'n': 3, 'b': 3, 'r': 5, 'q': 9, 'k': Infinity };
const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
const K_FACTOR = 32;
const TIME_SETTINGS = { // Time in seconds
standard: 600, // 10 minutes
blitz: 180, // 3 minutes
bullet: 60, // 1 minute <<< Added bullet time setting
unlimited: 999999 // Effectively unlimited (used as flag)
};
import { PUZZLE } from './puzzle.js';
// --- Game Instance (using chess.js) ---
let game = new Chess(); // The core game logic handler
// --- Game State Variables (Managed Externally or UI-Related) ---
let pieceRenderMode = 'png'; // 'png' or 'ascii'
let whiteTime = TIME_SETTINGS.standard; // Default time
let blackTime = TIME_SETTINGS.standard;
let timerInterval;
let moveHistoryUI = []; // Array of simple move notations for display { moveNumber, white, black }
let moveHistoryInternal = []; // Stores { fenBefore, moveSAN } for undo
let selectedSquareAlg = null; // Algebraic notation of selected square (e.g., 'e2')
let lastMoveHighlight = null; // { from: 'e2', to: 'e4' } algebraic notation
let isGameOver = false;
let gameMode = ''; // "human", "ai", "ai-vs-ai"
let selectedTimeMode = 'standard'; // 'standard', 'blitz', 'bullet', 'unlimited', 'custom' <<< Added 'custom'
let customInitialTime = 0; // Custom time in seconds <<< Added
let customIncrement = 0; // Custom increment in seconds <<< Added
let aiDifficulty = '';
let aiDifficultyWhite = '';
let aiDifficultyBlack = '';
let capturedWhite = []; // Store piece chars ('P', 'N', etc.) captured BY BLACK
let capturedBlack = []; // Store piece chars ('p', 'n', etc.) captured BY WHITE
let promotionCallback = null; // Stores the callback for promotion choice
let isReviewing = false; // Flag for game review state
let autoFlipEnabled = true; // Default to auto-flipping in H vs H mode
// --- Statistics & Ratings ---
let gamesPlayed = 0, wins = 0, losses = 0, draws = 0;
let playerRating = 1200;
let aiRating = 1200; // Generic AI rating, could be specific per difficulty later
// --- Stockfish Worker ---
let stockfish;
let isStockfishReady = false;
let isStockfishThinking = false;
let aiDelayEnabled = true; // Active ou désactive le délai pour l'IA
const AI_DELAY_TIME = 1000; // Délai en millisecondes (1s) - Reduced slightly
// --- UI Elements (Cache them) ---
const whiteTimeEl = document.getElementById('white-time');
const blackTimeEl = document.getElementById('black-time');
const gameStatusEl = document.getElementById('game-status');
const capturedWhiteEl = document.getElementById('captured-white');
const capturedBlackEl = document.getElementById('captured-black');
const whiteProgressEl = document.getElementById('white-progress');
const blackProgressEl = document.getElementById('black-progress');
const scoreAdvantageEl = document.getElementById('score-advantage');
const playerInfoWhiteEl = document.querySelector('.player-info-white');
const playerInfoBlackEl = document.querySelector('.player-info-black');
const player1RatingEl = playerInfoWhiteEl?.querySelector('.player-rating'); // More specific selectors
const player2RatingEl = playerInfoBlackEl?.querySelector('.player-rating');
const player1NameEl = playerInfoWhiteEl?.querySelector('.player-name');
const player2NameEl = playerInfoBlackEl?.querySelector('.player-name');
const moveListEl = document.getElementById('move-list');
const undoButton = document.getElementById('undo-button');
const resignButton = document.getElementById('resign-button');
const analyzeButton = document.getElementById('analyze-button'); // New analyze button
const exportButton = document.getElementById('export-button'); // New export button
const promotionModal = document.getElementById('promotion-modal');
const promotionOptionsContainer = promotionModal ? promotionModal.querySelector('.promotion-options') : null;
const gameEndModal = document.getElementById('game-end-modal');
const gameEndMessageEl = document.getElementById('game-end-message');
const playAgainButton = document.getElementById('play-again'); // Replay with same settings
const mainMenuButton = document.getElementById('main-menu-button'); // Go back to main menu
const analyzeGameModalButton = document.getElementById('analyze-game-modal-button'); // Analyze from modal
const themeToggleButton = document.getElementById('theme-toggle');
const soundToggleButton = document.getElementById('sound-toggle');
const pieceRenderToggle = document.getElementById('piece-render-toggle');
const aiDelayToggle = document.getElementById('ai-delay-toggle');
const mainMenuEl = document.getElementById('main-menu');
const timeSelectionEl = document.getElementById('time-selection'); // New time selection menu
const difficultySelectionEl = document.getElementById('difficulty-selection');
const aiVsAiDifficultySelectionEl = document.getElementById('ai-vs-ai-difficulty-selection');
const gameLayoutEl = document.querySelector('.game-layout'); // Main game area container
const statsContainerEl = document.getElementById('statistics'); // Stats container
const customTimeModal = document.getElementById('custom-time-modal'); // <<< Assume this exists
const customMinutesInput = document.getElementById('custom-minutes'); // <<< Assume this exists
const customSecondsInput = document.getElementById('custom-seconds'); // <<< Assume this exists
const customIncrementInput = document.getElementById('custom-increment'); // <<< Assume this exists
const confirmCustomTimeButton = document.getElementById('confirm-custom-time'); // <<< Assume this exists
const backToTimeFromCustomButton = document.getElementById('back-to-time-from-custom'); // <<< Added
// Back buttons
const backToModeButton = document.getElementById('back-to-mode');
const backToModeAivsAiButton = document.getElementById('back-to-mode-aivsai');
const backToTimeButton = document.getElementById('back-to-time');
// --- Helper Functions ---
function coordToAlg(row, col) {
return files[col] + (8 - row);
}
function algToCoord(alg) {
if (!alg || alg.length < 2) return null;
const col = files.indexOf(alg[0]);
const row = 8 - parseInt(alg[1]);
if (col === -1 || isNaN(row) || row < 0 || row > 7) return null;
return [row, col];
}
function chessjsPieceToMyFormat(pieceInfo) {
if (!pieceInfo) return '';
return pieceInfo.color === 'w' ? pieceInfo.type.toUpperCase() : pieceInfo.type.toLowerCase();
}
function preloadAllSounds() {
const soundNames = ['move', 'move2', 'capture', 'castle', 'check', 'click',
'promote', 'illegal', 'start', 'win', 'lose', 'draw', 'end', 'tenseconds'];
soundNames.forEach(name => loadSound(name, getSoundPath(name)));
}
function getSoundPath(name) {
const soundPaths = {
move: 'sounds/move-self.mp3', move2: 'sounds/move-opponent.mp3', capture: 'sounds/capture.mp3',
castle: 'sounds/castle.mp3', check: 'sounds/move-check.mp3', click: 'sounds/click.mp3',
promote: 'sounds/promote.mp3', illegal: 'sounds/illegal.mp3', start: 'sounds/game-start.mp3',
win: 'sounds/game-win.mp3', lose: 'sounds/game-lose.mp3', draw: 'sounds/game-draw.mp3',
end: 'sounds/game-end.mp3', tenseconds: 'sounds/tenseconds.mp3'
};
return soundPaths[name] || '';
}
// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
// Cache essential elements early
const flipBoardToggle = document.getElementById('flip-board-toggle'); // Cache here
initStockfish();
setupMenusAndButtons();
loadSavedSettings(); // Now flipBoardToggle exists when this calls updateFlipButtonVisualState
updateStatistics(); // Load potentially saved stats
updateRatingDisplay(); // Initial display based on defaults
preloadAllSounds();
if (gameStatusEl) gameStatusEl.textContent = "Choisissez un mode de jeu.";
else console.error("Element with ID 'game-status' not found.");
// Check essential elements
const essentialElements = {
mainMenuEl, timeSelectionEl, difficultySelectionEl, aiVsAiDifficultySelectionEl,
gameEndModal, promotionModal, promotionOptionsContainer, chessboard,
playerInfoWhiteEl, playerInfoBlackEl, gameLayoutEl, statsContainerEl,
analyzeButton, exportButton, analyzeGameModalButton, mainMenuButton
};
for (const key in essentialElements) {
if (!essentialElements[key]) console.error(`Essential element missing: ${key}`);
}
if (pieceRenderToggle) pieceRenderToggle.addEventListener('click', togglePieceRenderMode);
else console.warn("Piece render toggle button not found.");
if (aiDelayToggle) {
aiDelayToggle.addEventListener('click', toggleAIDelay);
aiDelayToggle.innerHTML = `<i class="fas fa-clock"></i> ${aiDelayEnabled ? 'ON' : 'OFF'}`; // Initial state with icon
} else console.warn("Bouton 'ai-delay-toggle' non trouvé.");
// Event listener setup for flipBoardToggle (no need to re-assign here)
if (flipBoardToggle) {
flipBoardToggle.addEventListener('click', toggleAutoFlip);
// updateFlipButtonVisualState(); // This is called within loadSavedSettings now
} else console.warn("Button 'flip-board-toggle' not found.");
// Hide game layout initially
if (gameLayoutEl) gameLayoutEl.style.display = 'none';
if (statsContainerEl) statsContainerEl.style.display = 'none';
});
// --- Setup Functions ---
function setupMenusAndButtons() {
// Main Menu Mode Buttons
const modeButtons = [
{ id: 'mode-ai', mode: 'ai' },
{ id: 'mode-human', mode: 'human' },
{ id: 'mode-ai-ai', mode: 'ai-vs-ai' }
];
modeButtons.forEach(({ id, mode }) => {
const button = document.getElementById(id);
if (button) button.addEventListener('click', () => setupGameMode(mode));
else console.warn(`Button '${id}' not found.`);
});
// Add analysis board button event listener
const analysisButton = document.getElementById('mode-analysis-board');
if (analysisButton) {
analysisButton.addEventListener('click', () => {
// Reset the main game state before navigating
returnToMainMenu();
// Clear any previously saved game intended for review
localStorage.removeItem('reviewGamePGN');
// Navigate to the review page (analysis board)
window.location.href = 'review.html';
});
} else {
console.warn("Button 'mode-analysis-board' not found.");
}
// Puzzle Mode Button
const puzzleModeButton = document.getElementById('mode-puzzle');
if (puzzleModeButton) puzzleModeButton.addEventListener('click', () => setupGameMode('puzzle'));
else console.warn("Button 'mode-puzzle' not found.");
// Add listeners for puzzle buttons
const hintButton = document.getElementById('hint-button');
if (hintButton) hintButton.addEventListener('click', showHint);
else console.warn("Button 'hint-button' not found.");
const nextPuzzleButton = document.getElementById('next-puzzle-button');
if (nextPuzzleButton) nextPuzzleButton.addEventListener('click', startPuzzleSession);
else console.warn("Button 'next-puzzle-button' not found.");
const exitPuzzleButton = document.getElementById('exit-puzzle-button');
if (exitPuzzleButton) exitPuzzleButton.addEventListener('click', returnToMainMenu);
else console.warn("Button 'exit-puzzle-button' not found.");
// Time Selection Buttons
if (timeSelectionEl) {
timeSelectionEl.querySelectorAll('.time-button').forEach(button => {
button.addEventListener('click', () => {
const timeMode = button.dataset.time;
playSound('click'); // Play sound on time selection
if (timeMode === 'custom') {
// Show custom time modal
if (customTimeModal && customMinutesInput && customSecondsInput && customIncrementInput) {
// Reset inputs to defaults or last used values if desired
customMinutesInput.value = localStorage.getItem('customMinutes') || '10';
customSecondsInput.value = localStorage.getItem('customSeconds') || '0';
customIncrementInput.value = localStorage.getItem('customIncrement') || '0';
customTimeModal.classList.add('show'); // <<< Use classList.add instead of style.display
// timeSelectionEl.style.display = 'none'; // Hiding this might be handled by showScreen or similar logic later
showScreen(customTimeModal, [timeSelectionEl]); // Use showScreen to manage visibility
} else {
console.error("Custom time modal or its input elements not found!");
showToast("Erreur: Impossible d'ouvrir le modal de temps personnalisé.", 'error');
}
} else {
selectedTimeMode = timeMode;
console.log(`Time mode selected: ${selectedTimeMode}`);
// Proceed based on game mode
if (gameMode === 'ai') {
showScreen(difficultySelectionEl, [timeSelectionEl]);
} else if (gameMode === 'human') {
// Directly start game for Human vs Human after time selection
startGame();
} else {
// Should not happen if menus are structured correctly
console.error("Invalid state after time selection.");
returnToMainMenu();
}
}
});
});
} else console.error("Time selection element not found.");
// Custom Time Modal Confirm Button
if (confirmCustomTimeButton) {
confirmCustomTimeButton.addEventListener('click', () => {
const minutes = parseInt(customMinutesInput.value, 10) || 0;
const seconds = parseInt(customSecondsInput.value, 10) || 0;
customIncrement = parseInt(customIncrementInput.value, 10) || 0;
customInitialTime = (minutes * 60) + seconds;
// Basic validation
if (customInitialTime <= 0) {
showToast("Le temps initial doit être supérieur à 0.", 'fa-exclamation-circle');
return;
}
if (customIncrement < 0) {
showToast("L'incrément ne peut pas être négatif.", 'fa-exclamation-circle');
return;
}
selectedTimeMode = 'custom';
if (customTimeModal) customTimeModal.style.display = 'none'; // Hide modal
console.log(`DEBUG: Custom time confirmed. Mode: ${selectedTimeMode}, Initial: ${customInitialTime}s, Increment: ${customIncrement}s`); // <<< DEBUG LOG
// Proceed based on game mode
if (gameMode === 'ai') {
if (difficultySelectionEl) difficultySelectionEl.style.display = 'block';
} else if (gameMode === 'human') {
startGame();
}
});
} else {
console.warn("Confirm custom time button not found.");
}
// Difficulty Selections (Player vs AI)
if (difficultySelectionEl) {
difficultySelectionEl.querySelectorAll('.difficulty-button').forEach(button => {
button.addEventListener('click', () => {
aiDifficulty = button.dataset.difficulty;
difficultySelectionEl.style.display = 'none';
startGame(); // Start game after difficulty selection
});
});
}
// Difficulty Selections (AI vs AI)
if (aiVsAiDifficultySelectionEl) {
aiVsAiDifficultySelectionEl.querySelectorAll('.difficulty-button').forEach(button => {
button.addEventListener('click', () => handleAiVsAiDifficultySelection(button));
});
}
// Back Buttons
if (backToModeButton) backToModeButton.addEventListener('click', () => showScreen(mainMenuEl, [timeSelectionEl, customTimeModal])); // Hide custom modal too
if (backToModeAivsAiButton) backToModeAivsAiButton.addEventListener('click', () => showScreen(mainMenuEl, [aiVsAiDifficultySelectionEl]));
if (backToTimeButton) backToTimeButton.addEventListener('click', () => showScreen(timeSelectionEl, [difficultySelectionEl]));
if (backToTimeFromCustomButton) backToTimeFromCustomButton.addEventListener('click', () => showScreen(timeSelectionEl, [customTimeModal])); // <<< Added listener for back button in custom modal
// In-Game Controls
if (undoButton) undoButton.addEventListener('click', undoMove);
else console.warn("Button 'undo-button' not found.");
if (resignButton) resignButton.addEventListener('click', resignGame);
else console.warn("Button 'resign-button' not found.");
if (analyzeButton) analyzeButton.addEventListener('click', initiateGameReview); // Connect analyze button
else console.warn("Button 'analyze-button' not found.");
if (exportButton) exportButton.addEventListener('click', exportGamePGN); // Connect export button
else console.warn("Button 'export-button' not found.");
// Modals & Controls
if (playAgainButton) playAgainButton.onclick = startGame; // Replay same settings
else console.warn("Button 'play-again' not found.");
if (mainMenuButton) mainMenuButton.onclick = returnToMainMenu; // Back to main menu
else console.warn("Button 'main-menu-button' not found.");
if (analyzeGameModalButton) analyzeGameModalButton.onclick = initiateGameReview; // Analyze from modal
else console.warn("Button 'analyze-game-modal-button' not found.");
if (themeToggleButton) themeToggleButton.addEventListener('click', toggleTheme);
else console.warn("Button 'theme-toggle' not found.");
if (soundToggleButton) soundToggleButton.addEventListener('click', toggleSound);
else console.warn("Button 'sound-toggle' not found.");
// Promotion Modal Setup (Assuming HTML structure exists)
setupPromotionModal();
}
function toggleAutoFlip() {
autoFlipEnabled = !autoFlipEnabled;
console.log(`Auto-flip board set to: ${autoFlipEnabled}`);
localStorage.setItem('chess-auto-flip', autoFlipEnabled ? 'on' : 'off');
updateFlipButtonVisualState();
playSound('click');
// Redraw the board immediately if in a human vs human game
if (gameMode === 'human' && !isGameOver && !isReviewing) {
createBoard(); // Redraw with the current player's perspective based on the new setting
}
}
function updateFlipButtonVisualState() {
const flipBoardToggle = document.getElementById('flip-board-toggle'); // Re-get element or ensure it's passed/available globally
if (!flipBoardToggle) return;
// Example: Add/remove an 'active' class or change icon opacity/color
flipBoardToggle.classList.toggle('active', autoFlipEnabled);
// Or change the icon itself:
// const icon = flipBoardToggle.querySelector('i');
// if (icon) icon.className = autoFlipEnabled ? 'fas fa-sync-alt active' : 'fas fa-sync-alt';
}
// Helper to switch between menu/game screens
function showScreen(screenToShow, screensToHide = []) {
const allScreens = [
mainMenuEl, timeSelectionEl, difficultySelectionEl,
aiVsAiDifficultySelectionEl, gameLayoutEl, statsContainerEl,
customTimeModal // <<< Add custom modal here
];
allScreens.forEach(screen => {
if (screen) screen.style.display = 'none';
});
screensToHide.forEach(screen => {
if (screen) screen.style.display = 'none';
});
if (screenToShow) screenToShow.style.display = screenToShow.classList.contains('menu-container') ? 'block' : 'grid'; // Use grid for game layout
// Show stats container only when game layout is shown and mode is AI
if (screenToShow === gameLayoutEl && gameMode === 'ai' && statsContainerEl) {
statsContainerEl.style.display = 'block';
} else if (statsContainerEl) {
statsContainerEl.style.display = 'none';
}
}
function setupGameMode(mode) {
gameMode = mode;
console.log("Selected game mode:", mode);
showScreen(null, [mainMenuEl, timeSelectionEl, difficultySelectionEl, aiVsAiDifficultySelectionEl]); // Hide all menus
if (mode === 'ai' || mode === 'human') {
if (timeSelectionEl) timeSelectionEl.style.display = 'block';
else console.error("Time selection screen not found!");
} else if (mode === 'ai-vs-ai') {
if (aiVsAiDifficultySelectionEl) {
selectedTimeMode = 'unlimited';
aiVsAiDifficultySelectionEl.style.display = 'block';
aiDifficultyWhite = ''; aiDifficultyBlack = '';
aiVsAiDifficultySelectionEl.querySelectorAll('button.selected').forEach(b => b.classList.remove('selected'));
} else console.error("AI vs AI difficulty screen not found!");
} else if (mode === 'puzzle') {
// Directly start the puzzle session
startPuzzleSession();
}
}
function setupPromotionModal() {
// This function assumes the HTML for pieces is added dynamically by showPromotionModal
// It sets up the container interaction if needed, but click logic is now per-piece in showPromotionModal
if (!promotionModal) return;
// Close modal if clicking outside the content?
promotionModal.addEventListener('click', (event) => {
if (event.target === promotionModal) { // Clicked on backdrop
if (promotionCallback) {
promotionCallback(null); // Indicate cancellation
promotionCallback = null;
}
promotionModal.classList.remove('show');
// Re-enable board interaction if needed
}
});
}
function handleAiVsAiDifficultySelection(button) {
if (!aiVsAiDifficultySelectionEl) return;
const color = button.dataset.color;
const difficulty = button.dataset.difficulty;
const group = button.closest('.ai-diff-group');
if (!group) return;
// Deselect others in the same group
group.querySelectorAll('.difficulty-button').forEach(b => b.classList.remove('selected'));
button.classList.add('selected');
if (color === 'white') aiDifficultyWhite = difficulty;
else if (color === 'black') aiDifficultyBlack = difficulty;
// Check if both are selected
if (aiDifficultyWhite && aiDifficultyBlack) {
aiVsAiDifficultySelectionEl.style.display = 'none';
startGame(); // Start AI vs AI game
}
}
function returnToMainMenu() {
showGameEndModal(false);
document.body.classList.remove('puzzle-mode-active'); // Remove puzzle class
showScreen(mainMenuEl);
resetTimer();
isGameOver = true; // Mark any active game/puzzle as finished
clearInterval(timerInterval);
if (gameStatusEl) gameStatusEl.textContent = "Choisissez un mode de jeu.";
updateRatingDisplay();
resetBoardState(); // Full main game reset
game = new Chess(); // Reset main chess instance
showScreen(mainMenuEl);
document.body.classList.remove('game-active', 'puzzle-mode-active'); // Remove specific mode classes
// Stop the puzzle if it's running
if (gameMode === 'puzzle') {
PUZZLE.stopPuzzle(); // Call the stop method from the PUZZLE module
}
// Reset progress bar visibility if it was hidden
const progressBar = document.getElementById('progress-bar');
if (progressBar) progressBar.style.visibility = 'visible';
// Reset puzzle-specific button visibility (handled by showScreen usually, but belt-and-suspenders)
if (hintButton) hintButton.style.display = 'none';
if (nextPuzzleButton) nextPuzzleButton.style.display = 'none';
if (exitPuzzleButton) exitPuzzleButton.style.display = 'none';
// Ensure standard controls are potentially visible again (updateControlsState will refine)
if (undoButton) undoButton.style.display = 'flex';
if (resignButton) resignButton.style.display = 'flex';
// etc. for analyze/export
}
function resetBoardState() {
game = new Chess(); // Reset the game state using chess.js default start position
moveHistoryUI = [];
moveHistoryInternal = [];
selectedSquareAlg = null;
lastMoveHighlight = null;
isGameOver = false;
capturedWhite.length = 0;
capturedBlack.length = 0;
isStockfishThinking = false;
isReviewing = false;
promotionCallback = null;
if (moveListEl) moveListEl.innerHTML = '';
updateGameStatus("Nouvelle partie !");
if(chessboard) chessboard.innerHTML = ''; // Clear board visually
updateCapturedPieces();
updateProgressBar();
updateTimerDisplay(); // Reset display
updateControlsState();
updatePlayerTurnIndicator();
}
function loadSavedSettings() {
// Theme
const savedTheme = localStorage.getItem('chess-theme');
const body = document.body;
const themeIcon = themeToggleButton ? themeToggleButton.querySelector('i') : null;
body.classList.toggle('light-theme', savedTheme === 'light');
if (themeIcon) {
themeIcon.className = savedTheme === 'light' ? 'fas fa-sun' : 'fas fa-moon';
}
// Sound
const soundSetting = localStorage.getItem('chess-sound');
const soundIcon = soundToggleButton ? soundToggleButton.querySelector('i') : null;
soundEnabled = (soundSetting !== 'off');
if (soundIcon) {
soundIcon.className = soundEnabled ? 'fas fa-volume-up' : 'fas fa-volume-mute';
}
// AI Delay
const delaySetting = localStorage.getItem('chess-ai-delay');
aiDelayEnabled = (delaySetting !== 'off');
if (aiDelayToggle) {
aiDelayToggle.innerHTML = `${aiDelayEnabled ? 'ON' : 'OFF'}`;
}
// Piece Render Mode
const renderSetting = localStorage.getItem('chess-render-mode');
pieceRenderMode = (renderSetting === 'ascii') ? 'ascii' : 'png'; // Default to png
// Update button icon?
const renderIcon = pieceRenderToggle?.querySelector('i');
if (renderIcon) {
// Maybe change icon based on mode? e.g., fa-font vs fa-image
renderIcon.className = pieceRenderMode === 'ascii' ? 'fas fa-font' : 'fas fa-chess-pawn';
}
// Load stats (simple example, only player rating for now)
const savedRating = localStorage.getItem('chess-player-rating');
if (savedRating) {
playerRating = parseInt(savedRating, 10) || 1200;
}
// Could load gamesPlayed, wins etc. similarly
const flipSetting = localStorage.getItem('chess-auto-flip');
// Default to 'on' if not set or invalid
autoFlipEnabled = (flipSetting === 'off') ? false : true;
updateFlipButtonVisualState();
}
function togglePieceRenderMode() {
pieceRenderMode = (pieceRenderMode === 'ascii') ? 'png' : 'ascii';
localStorage.setItem('chess-render-mode', pieceRenderMode);
const renderIcon = pieceRenderToggle?.querySelector('i');
if (renderIcon) {
renderIcon.className = pieceRenderMode === 'ascii' ? 'fas fa-font' : 'fas fa-chess-pawn';
}
createBoard(); // Redraw board with new mode
console.log(`Piece render mode switched to: ${pieceRenderMode}`);
}
function toggleAIDelay() {
aiDelayEnabled = !aiDelayEnabled;
console.log(`AI Delay ${aiDelayEnabled ? 'Activé' : 'Désactivé'}`);
if (aiDelayToggle) {
aiDelayToggle.innerHTML = `<i class="fas fa-clock"></i> ${aiDelayEnabled ? 'ON' : 'OFF'}`;
}
localStorage.setItem('chess-ai-delay', aiDelayEnabled ? 'on' : 'off');
}
// --- Game Flow & Control ---
function startGame() {
console.log(`DEBUG: startGame called. Mode=${gameMode}, TimeMode=${selectedTimeMode}, AI=${aiDifficulty || (aiDifficultyWhite + '/' + aiDifficultyBlack)}`); // <<< DEBUG LOG
if (selectedTimeMode === 'custom') {
console.log(`DEBUG: startGame custom settings: Initial=${customInitialTime}s, Increment=${customIncrement}s`); // <<< DEBUG LOG
}
showGameEndModal(false); // Ensure end modal is hidden
resetBoardState(); // Clean state
// Set initial time based on selection
if (selectedTimeMode === 'custom') {
whiteTime = customInitialTime;
blackTime = customInitialTime;
} else if (selectedTimeMode === 'unlimited') {
whiteTime = TIME_SETTINGS.unlimited;
blackTime = TIME_SETTINGS.unlimited;
} else {
whiteTime = TIME_SETTINGS[selectedTimeMode] || TIME_SETTINGS.standard;
blackTime = TIME_SETTINGS[selectedTimeMode] || TIME_SETTINGS.standard;
}
console.log(`DEBUG: startGame initial times set: W=${whiteTime}, B=${blackTime}`); // <<< DEBUG LOG
updateTimerDisplay(); // Show initial time
showScreen(gameLayoutEl); // Show the main game layout
createBoard(); // Draw the initial board
updateAllUI(); // Update captured, progress, timers, ratings
startTimer();
playSound('start');
if (gameMode === 'ai-vs-ai') {
if (!aiDifficultyWhite || !aiDifficultyBlack) {
console.error("AI vs AI mode but difficulties not set.");
updateGameStatus("Erreur: Difficultés IA non définies.");
returnToMainMenu(); // Go back if config error
return;
}
// AI vs AI starts immediately (or after short delay) if Stockfish ready
setTimeout(() => {
if (!isGameOver && isStockfishReady && game.turn() === 'w') requestAiMove();
}, 500);
} else if (gameMode === 'ai' && game.turn() === 'b') {
// Should not happen on fresh start, but handle just in case
setTimeout(() => {
if (!isGameOver && isStockfishReady) requestAiMove();
}, 500);
} else {
updateGameStatus("Les blancs commencent.");
}
updateControlsState(); // Set initial button states
updatePlayerTurnIndicator();
updateRatingDisplay(); // Update names/ratings based on mode
}
function updatePlayerTurnIndicator(puzzlePlayerTurnColor = null) {
if (!playerInfoWhiteEl || !playerInfoBlackEl) return;
let whiteActive = false;
let blackActive = false;
if (gameMode === 'puzzle') {
// Highlight based on the puzzle's player turn, passed in
const puzzleInfo = PUZZLE.getCurrentPuzzleData();
if (puzzleInfo?.isPlayerTurn) {
if (puzzleInfo.playerColor === 'w') whiteActive = true;
else blackActive = true;
}
} else if (!isGameOver) {
// Original logic for AI/Human modes
const currentTurn = game.turn();
whiteActive = currentTurn === 'w';
blackActive = currentTurn === 'b';
}
playerInfoWhiteEl.classList.toggle('active-player', whiteActive);
playerInfoBlackEl.classList.toggle('active-player', blackActive);
}
function endGame(winner, reason) {
if (isGameOver) return; // Prevent multiple calls
isGameOver = true;
clearInterval(timerInterval);
if (isStockfishThinking && stockfish) {
stockfish.postMessage('stop'); // Try to stop Stockfish if it was thinking
isStockfishThinking = false;
}
gamesPlayed++;
let message = '';
let sound = 'end';
let playerWonVsAI = null; // null for draw, true for player win, false for AI win
if (winner === 'draw') {
draws++;
message = `Match nul (${reason}).`;
sound = 'draw';
playerWonVsAI = null;
} else {
const winnerColorText = winner === 'white' ? 'Blancs' : 'Noirs';
message = `Victoire des ${winnerColorText} (${reason}).`;
if (gameMode === 'ai') {
// Assuming player is always White vs AI for rating purposes
if (winner === 'white') {
wins++;
sound = 'win';
showConfetti();
playerWonVsAI = true;
} else {
losses++;
sound = 'lose';
playerWonVsAI = false;
}
updateRatings(playerWonVsAI); // Update Elo based on result vs AI
} else if (gameMode === 'human') {
sound = (winner === 'white') ? 'win' : 'lose'; // Simple win/lose sounds
if (winner === 'white') showConfetti(); // Confetti for white win
} else { // AI vs AI
sound = 'end';
}
}
updateStatistics(); // Update stats display
updateRatingDisplay(); // Display potentially changed Elo
updateGameStatus(message); // Show final status on board wrapper
showGameEndModal(true, message); // Show the modal with the result
playSound(sound);
updateControlsState(); // Disable undo/resign, enable analyze/export
updatePlayerTurnIndicator(); // Clear active player highlight
}
function resignGame() {
if (isGameOver || gameMode === 'ai-vs-ai' || isReviewing) return; // Cannot resign AIvAI or during review
const loserColor = game.turn() === 'w' ? 'Blancs' : 'Noirs';
const winner = game.turn() === 'w' ? 'black' : 'white';
updateGameStatus(`Les ${loserColor} abandonnent.`);
endGame(winner, 'abandon');
}
function updateControlsState() {
const historyExists = moveHistoryInternal.length > 0;
const canUndo = historyExists && !isGameOver && !isStockfishThinking && !isReviewing && gameMode !== 'ai-vs-ai';
// Allow resign unless game over, AIvAI, or reviewing
const canResign = !isGameOver && !isReviewing && gameMode !== 'ai-vs-ai';
// Allow analyze only when game is over and not already reviewing
const canAnalyze = isGameOver && !isReviewing && historyExists;
// Allow export if history exists and not reviewing (allow during game)
const canExport = historyExists && !isReviewing;
if (undoButton) undoButton.disabled = !canUndo;
if (resignButton) resignButton.disabled = !canResign;
if (analyzeButton) analyzeButton.disabled = !canAnalyze; // Button in game controls is for post-game analysis now
if (exportButton) exportButton.disabled = !canExport;
// Also update modal analyze button state
if (analyzeGameModalButton) analyzeGameModalButton.disabled = !canAnalyze;
}
// --- Move History & Notation (UI specific) ---
function updateMoveListUI(moveNumber, moveSAN, turn) {
if (!moveListEl) return;
const moveIndex = moveHistoryInternal.length - 1; // Correlates with internal history index
if (turn === 'w') { // White moved
const listItem = document.createElement('li');
listItem.dataset.moveIndex = moveIndex;
listItem.innerHTML = `<span class="move-number">${moveNumber}.</span> <span class="move-white">${moveSAN}</span>`;
moveListEl.appendChild(listItem);
} else { // Black moved
let lastItem = moveListEl.lastElementChild;
// Ensure we are adding to the correct move number item
if (lastItem && lastItem.dataset.moveIndex == moveIndex -1 && lastItem.querySelectorAll('.move-black').length === 0) {
const blackMoveSpan = document.createElement('span');
blackMoveSpan.className = 'move-black';
blackMoveSpan.textContent = moveSAN;
lastItem.appendChild(document.createTextNode(' ')); // Add space
lastItem.appendChild(blackMoveSpan);
} else {
// If white didn't move first (e.g., loaded FEN?) or issue, create new item
const listItem = document.createElement('li');
listItem.dataset.moveIndex = moveIndex;
// Use '...' if black moved first in the number pair technically
listItem.innerHTML = `<span class="move-number">${moveNumber}. ...</span> <span class="move-black">${moveSAN}</span>`;
moveListEl.appendChild(listItem);
}
}
moveListEl.scrollTop = moveListEl.scrollHeight; // Auto-scroll
}
// --- Undo Logic ---
function undoMove() {
// Allow undo only if not game over, not AI thinking/reviewing, not AIvsAI, and history exists
if (isGameOver || isStockfishThinking || isReviewing || gameMode === 'ai-vs-ai' || moveHistoryInternal.length === 0) {
playSound('illegal');
return;
}
let movesToUndo = 1;
// In Player vs AI mode, if it's currently Player's turn (meaning AI just moved), undo both moves.
if (gameMode === 'ai' && game.turn() === 'w' && moveHistoryInternal.length >= 2) {
movesToUndo = 2;
}
console.log(`Attempting to undo ${movesToUndo} move(s).`);
for (let i = 0; i < movesToUndo; i++) {
if (moveHistoryInternal.length === 0) break;
const undoneMoveChessjs = game.undo(); // Undo in chess.js
if (!undoneMoveChessjs) {
console.error("chess.js undo failed! History might be corrupted.");
showToast("Erreur lors de l'annulation.", 'fa-times-circle', 4000);
return; // Stop undo process
}
// Remove from our internal history tracker
moveHistoryInternal.pop();
// Restore captured pieces list based on chess.js undo info
if (undoneMoveChessjs.captured) {
// Piece color determines who captured: if white moved (undoneMoveChessjs.color === 'w'), they captured a black piece.
const capturedPieceFormatted = undoneMoveChessjs.color === 'w'
? undoneMoveChessjs.captured.toLowerCase() // White captured black piece ('p'), remove from capturedBlack
: undoneMoveChessjs.captured.toUpperCase(); // Black captured white piece ('P'), remove from capturedWhite
const targetArray = undoneMoveChessjs.color === 'w' ? capturedBlack : capturedWhite;
const index = targetArray.lastIndexOf(capturedPieceFormatted);
if (index > -1) {
targetArray.splice(index, 1);
console.log(`Undo: Restored captured piece '${capturedPieceFormatted}' from list.`);
} else {
console.warn(`Undo: Could not find captured piece '${capturedPieceFormatted}' in corresponding capture list.`);
}
}
}
// --- Update UI After Undo ---
// Get the last move from chess.js history *after* undo
const lastMoveVerbose = game.history({ verbose: true });
lastMoveHighlight = lastMoveVerbose.length > 0
? { from: lastMoveVerbose[lastMoveVerbose.length - 1].from, to: lastMoveVerbose[lastMoveVerbose.length - 1].to }
: null;
createBoard(); // Redraw based on restored game state
updateAllUI(); // Update captured, progress, timers, ratings, turn indicator
const currentTurnColor = game.turn() === 'w' ? 'Blancs' : 'Noirs';
updateGameStatus(`Coup(s) annulé(s). Au tour des ${currentTurnColor}.`);
updateControlsState();
checkAndUpdateKingStatus(); // Update check highlight
// Remove the last move(s) from the UI list
if (moveListEl) {
for (let i = 0; i < movesToUndo; i++) {
if(moveListEl.lastElementChild) {
// Check if the last element contains both white and black moves
const lastLi = moveListEl.lastElementChild;
const hasWhiteMove = lastLi.querySelector('.move-white');
const hasBlackMove = lastLi.querySelector('.move-black');
// If we are undoing black's move (movesToUndo=1 and last move was black, or movesToUndo=2 and this is the second undo)
// and the LI contains both white and black, just remove black.
// This logic is tricky. Easier: always remove the last element if undoing white,
// or remove the black span if undoing black from a combined LI.
if (i === 0 && movesToUndo === 1 && game.turn() === 'b') { // Undoing White's move
if(lastLi) lastLi.remove();
} else if (i === 0 && movesToUndo === 2 && game.turn() === 'w') { // Undoing Black's move (first of two)
if(hasBlackMove) {
hasBlackMove.previousSibling?.remove(); // Remove space before black move
hasBlackMove.remove();
} else if (lastLi) {
lastLi.remove(); // Should not happen normally if black moved last
}
} else if (i === 1 && movesToUndo === 2 && game.turn() === 'b') { // Undoing White's move (second of two)
if(lastLi) lastLi.remove();
}
else { // Fallback or simpler logic: just remove the last LI element per undo
if(lastLi) lastLi.remove();
}
}
}
moveListEl.scrollTop = moveListEl.scrollHeight; // Scroll after removing
}
playSound('click');
console.log("Undo complete. Current FEN:", game.fen());
}
// --- PGN Export ---
function exportGamePGN() {
if (game.history().length === 0) {
showToast("Aucun coup joué à exporter.", 'fa-info-circle');
return;
}
if (isReviewing) {
showToast("Veuillez attendre la fin de l'analyse.", 'fa-hourglass-half');
return;
}
try {
// Add standard PGN headers
const pgnHeaders = {
Event: "Partie locale",
Site: "DFWS Chess App",
Date: new Date().toISOString().split('T')[0], // YYYY-MM-DD
Round: gamesPlayed.toString(), // Or move number?
White: player1NameEl?.textContent || "Joueur Blanc",
Black: player2NameEl?.textContent || "Joueur Noir",
Result: isGameOver ? gameResultToPGN(game) : "*" // Determine result if game is over
// Add TimeControl if needed: e.g., '180+0' for blitz, '600+0' for standard, '60+0' for bullet, '600+5' for custom
};
if (selectedTimeMode !== 'unlimited' && gameMode !== 'ai-vs-ai') {
const initialTime = selectedTimeMode === 'custom' ? customInitialTime : TIME_SETTINGS[selectedTimeMode];
const increment = selectedTimeMode === 'custom' ? customIncrement : 0;
pgnHeaders.TimeControl = `${initialTime}+${increment}`;
}
if (gameMode === 'ai') {
pgnHeaders.WhiteElo = playerRating.toString();
pgnHeaders.BlackElo = aiRating.toString(); // Use generic AI rating or difficulty-based
}
const pgn = game.pgn({ headers: pgnHeaders });
const blob = new Blob([pgn], { type: 'application/x-chess-pgn;charset=utf-8' }); // Correct MIME type
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// Suggest filename
const dateStr = new Date().toISOString().replace(/[:\-]/g, '').slice(0, 8);
const filenameSafeWhite = (pgnHeaders.White || 'White').replace(/[^a-z0-9]/gi, '_').toLowerCase();
const filenameSafeBlack = (pgnHeaders.Black || 'Black').replace(/[^a-z0-9]/gi, '_').toLowerCase();
a.download = `dfws_chess_${filenameSafeWhite}_vs_${filenameSafeBlack}_${dateStr}.pgn`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast("Partie exportée en PGN.", 'fa-download');
} catch (error) {
console.error("Failed to generate PGN:", error);
showToast("Erreur lors de l'exportation PGN.", 'fa-times-circle');
}
}
// Helper to get PGN result string
function gameResultToPGN(gameInstance) {
if (!gameInstance.game_over()) return "*";
if (gameInstance.in_checkmate()) {
return gameInstance.turn() === 'b' ? "1-0" : "0-1"; // Winner is opposite of whose turn it is
}
if (gameInstance.in_draw() || gameInstance.in_stalemate() || gameInstance.in_threefold_repetition() || gameInstance.insufficient_material()) {
return "1/2-1/2";
}
// Add cases for specific draw types if needed
return "*"; // Default if somehow game_over but no specific condition met
}
const difficultyRatings = {
'Learn': 600,
'Noob': 800,
'Easy': 1000,
'Regular': 1200,
'Hard': 1400,
'Very Hard': 1600,
'Super Hard': 1800,
'Magnus Carlsen': 2850,
'Unbeatable': 3000,
'Adaptative': aiRating,
'AI100': 100,
'AI200': 200
};
// --- Game Review (Analysis) --- MODIFIED ---
function initiateGameReview() {
// Vérifier que l'analyse est possible
if (isReviewing) {
showToast("Analyse déjà en cours.", 'fa-hourglass-half');
return;
}
if (!isGameOver) {
showToast("L'analyse est disponible après la fin de la partie.", 'fa-info-circle');
return;
}
if (game.history().length === 0) {
showToast("Aucun coup à analyser.", 'fa-info-circle');
return;
}
if (!isStockfishReady) {
showToast("Moteur d'analyse non prêt.", 'fa-cog');
}
console.log("--- Initiating Game Review ---");
showGameEndModal(false); // Masquer le modal de fin
const difficultyRatings = {
'Learn': 600,
'Noob': 800,
'Easy': 1000,