-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
823 lines (783 loc) · 73.4 KB
/
Copy pathindex.html
File metadata and controls
823 lines (783 loc) · 73.4 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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OmniChess</title>
<link rel="icon" type="image/png" href="icon-64.png">
<meta property="og:title" content="OmniChess">
<meta property="og:description" content="Fully working Atomic variant – captures explode all adjacent pieces including kings.">
<meta property="og:image" content="https://coder787-source.github.io/OmniChess/icon-64.png">
<meta property="og:url" content="https://coder787-source.github.io/OmniChess/">
<meta property="og:type" content="website">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background:#1e1e1e; color:#ecf0f1; font-family:'Segoe UI',Arial,sans-serif; height:100vh; overflow:hidden; user-select:none; }
#credit-bar { position:fixed; top:5px; left:50%; transform:translateX(-50%); font-size:11px; color:#555555; font-family:'Courier New',monospace; z-index:10; white-space:nowrap; }
/* ── MENU ── */
#menu { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; gap:14px; }
#menu-title { font-size:50px; font-weight:bold; color:#f1c40f; margin-bottom:4px; }
.menu-row { display:flex; gap:18px; align-items:flex-start; }
.panel { background:#262421; border:2px solid #45423e; border-radius:10px; padding:14px 16px; }
.panel-label { font-size:13px; font-weight:bold; color:#ecf0f1; margin-bottom:10px; letter-spacing:.5px; }
#level-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; width:260px; }
.btn { background:#45423e; border:none; border-radius:8px; color:#ecf0f1; font-family:inherit; font-size:15px; font-weight:bold; padding:10px 14px; cursor:pointer; transition:background .12s; }
.btn:hover { background:#5a5652; }
.btn.active { background:#f1c40f; color:#1e1e1e; }
.btn.win { background:#2ecc71; color:#fff; }
.btn.win:hover { background:#27ae60; }
.btn.accent { background:#f1c40f; color:#1e1e1e; }
.btn.accent:hover { background:#d4ac0d; }
.btn.teal { background:#1baca1; color:#fff; }
.btn.teal:hover { background:#178f86; }
.btn.loss { background:#ff4757; color:#fff; }
.btn:disabled { opacity:.45; cursor:default; }
.btn.variant { background:#9b59b6; color:#fff; }
.btn.variant:hover { background:#8e44ad; }
#elo-value { font-size:40px; font-weight:bold; text-align:center; padding:4px 0; font-family:'Courier New',monospace; }
.elo-btns { display:flex; gap:6px; justify-content:center; }
.color-row { display:flex; gap:8px; justify-content:center; margin-top:6px; }
#timer-value { font-size:34px; font-weight:bold; color:#f1c40f; text-align:center; font-family:'Courier New',monospace; margin:4px 0; }
.timer-btns { display:flex; gap:6px; justify-content:center; }
#theme-hint { font-size:12px; color:#888888; text-align:center; }
#theme-hint span { color:#f1c40f; }
.menu-actions { display:flex; gap:12px; }
.menu-actions .btn { font-size:16px; padding:12px 22px; }
/* ── GAME ── */
#game { display:none; width:100vw; height:100vh; align-items:center; justify-content:center; flex-direction:row; background:#1e1e1e; }
#board-area { display:flex; flex-direction:column; }
#board-and-ranks { display:flex; flex-direction:row; }
#rank-labels { display:flex; flex-direction:column; width:14px; height:600px; }
.rank-label { font-size:12px; color:#888888; font-family:'Courier New',monospace; text-align:center; flex:1; display:flex; align-items:center; justify-content:center; }
canvas#board { display:block; cursor:pointer; }
#file-labels { display:flex; flex-direction:row; width:600px; margin-left:14px; margin-top:2px; }
.file-label { flex:1; text-align:center; font-size:12px; color:#888888; font-family:'Courier New',monospace; }
#opening-display { font-size:13px; color:#aaddff; margin-top:4px; margin-left:14px; min-height:18px; font-family:'Courier New',monospace; }
#season-display { font-size:12px; color:#f1c40f; margin-left:14px; min-height:16px; }
/* ── SIDEBAR ── */
#sidebar { width:350px; margin-left:40px; display:flex; flex-direction:column; gap:8px; }
.player-card { background:#45423e; border-radius:10px; padding:10px 14px; min-height:100px; }
.player-label { font-size:14px; font-weight:bold; color:#f1c40f; }
.player-clock { font-size:38px; font-weight:bold; color:#ffffff; font-family:'Courier New',monospace; line-height:1.1; }
.player-captures { font-size:20px; min-height:26px; margin-top:2px; }
.move-history-box { background:#2c2c2c; border-radius:10px; padding:10px 14px; min-height:95px; }
.move-history-title { font-size:11px; color:#888888; font-weight:bold; letter-spacing:1px; margin-bottom:4px; }
#move-history-text {
font-size:13px; color:#ffffff; font-family:'Courier New',monospace; line-height:1.65;
display:grid; grid-template-columns:1fr 1fr; column-gap:6px;
}
.sidebar-btn-group { display:flex; flex-direction:column; gap:6px; }
.sidebar-btn-group .btn { width:100%; text-align:center; padding:12px; font-size:15px; }
/* ── OVERLAYS ── */
.overlay { position:fixed; inset:0; background:rgba(0,0,0,.7); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:50; gap:18px; }
.overlay-title { font-size:46px; font-weight:bold; color:#f1c40f; }
.promo-row { display:flex; gap:12px; }
.promo-btn { width:65px; height:65px; background:#45423e; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:38px; cursor:pointer; border:none; transition:background .12s; }
.promo-btn:hover { background:#f1c40f; }
/* ── PUZZLE ── */
#puzzle { display:none; width:100vw; height:100vh; align-items:center; justify-content:center; flex-direction:row; background:#1e1e1e; }
#puzzle-sidebar { width:350px; margin-left:40px; display:flex; flex-direction:column; gap:8px; }
.puzzle-info-card { background:#45423e; border-radius:10px; padding:12px 14px; }
.puz-lbl { font-size:14px; font-weight:bold; color:#f1c40f; }
.puz-sub { font-size:13px; color:#ffffff; margin-top:4px; }
.puz-tiny { font-size:12px; color:#aaaaaa; margin-top:2px; }
.puz-timer { font-size:12px; font-family:'Courier New',monospace; margin-top:2px; }
#puzzle-status-banner { border-radius:10px; padding:12px; text-align:center; font-size:17px; font-weight:bold; display:none; }
.banner-solved { background:#2ecc71; color:#fff; }
.banner-failed { background:#ff4757; color:#fff; }
#puzzle-feedback { font-size:12px; color:#aaddff; min-height:18px; font-family:'Courier New',monospace; }
#board-area-puzzle { display:flex; flex-direction:column; }
#board-and-ranks-puzzle { display:flex; flex-direction:row; }
#rank-labels-puzzle { display:flex; flex-direction:column; width:14px; height:600px; }
#file-labels-puzzle { display:flex; flex-direction:row; width:600px; margin-left:14px; margin-top:2px; }
canvas#board-puzzle { display:block; cursor:pointer; }
/* ── TOAST ── */
#toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:#262421; border:1px solid #f1c40f; color:#f1c40f; padding:9px 22px; border-radius:8px; font-size:13px; font-weight:bold; opacity:0; transition:opacity .3s; z-index:100; pointer-events:none; }
#toast.show { opacity:1; }
</style>
</head>
<body>
<div id="credit-bar">OmniChess:Entertainment Studios · ENGINE: Stockfish 18 · v2.8 · Author: The Stockfish Developers</div>
<!-- MENU -->
<div id="menu">
<div id="menu-title">OmniChess</div>
<div class="menu-row">
<div class="panel">
<div class="panel-label">ENGINE LEVEL</div>
<div id="level-grid"></div>
</div>
<div class="panel">
<div class="panel-label">CUSTOM ELO MODE</div>
<div id="elo-value" style="color:#ffffff">1500</div>
<div class="elo-btns">
<button class="btn" onclick="adjustElo(-100)">-100</button>
<button class="btn" onclick="adjustElo(-10)">-10</button>
<button class="btn" onclick="adjustElo(10)">+10</button>
<button class="btn" onclick="adjustElo(100)">+100</button>
</div>
</div>
</div>
<div style="text-align:center">
<div class="panel-label">PLAY AS:</div>
<div class="color-row">
<button class="btn active" id="btn-white" onclick="setUserColor('w')">WHITE</button>
<button class="btn" id="btn-black" onclick="setUserColor('b')">BLACK</button>
</div>
</div>
<div style="text-align:center">
<div class="panel-label">TIME CONTROL:</div>
<div id="timer-value">10:00</div>
<div class="timer-btns">
<button class="btn" onclick="adjustTimer(-60)">-60</button>
<button class="btn" onclick="adjustTimer(-10)">-10</button>
<button class="btn" onclick="adjustTimer(10)">+10</button>
<button class="btn" onclick="adjustTimer(60)">+60</button>
</div>
<div style="margin-top:8px;font-size:13px;font-weight:bold;color:#ecf0f1">INCREMENT: <span id="increment-value" style="color:#f1c40f">+0s/move</span></div>
<div class="timer-btns" style="margin-top:4px">
<button class="btn" style="padding:6px 10px;font-size:13px" onclick="adjustIncrement(-5)">-5</button>
<button class="btn" style="padding:6px 10px;font-size:13px" onclick="adjustIncrement(-1)">-1</button>
<button class="btn" style="padding:6px 10px;font-size:13px" onclick="adjustIncrement(1)">+1</button>
<button class="btn" style="padding:6px 10px;font-size:13px" onclick="adjustIncrement(5)">+5</button>
</div>
</div>
<div style="text-align:center">
<div class="panel-label">VARIANT: <span id="variant-name-display" style="color:#9b59b6">Standard</span></div>
<div style="font-size:12px;color:#888;margin-bottom:4px" id="variant-desc-display">Classic chess</div>
<button class="btn" style="background:#9b59b6;color:#fff;padding:8px 18px" onclick="cycleVariant()">CYCLE VARIANT</button>
</div>
<div id="theme-hint">T = Board Theme | S = Seasonal On/Off | Active: <span id="theme-name-display">New Year</span></div>
<div class="menu-actions">
<button class="btn teal" onclick="enterPuzzle()">DAILY PUZZLE</button>
<button class="btn win" onclick="startGame(false)">START</button>
<button class="btn accent" onclick="startGame(true)">LOCAL MULTIPLAYER</button>
</div>
<div style="margin-top:-6px">
<button class="btn variant" style="padding:10px 28px;font-size:15px" onclick="enterEveSetup()">ENGINE VS ENGINE</button>
<button class="btn" style="padding:10px 28px;font-size:15px" onclick="openSettings()">SETTINGS</button>
</div>
</div>
<!-- EVE SETUP -->
<div id="eve-setup" style="display:none;flex-direction:column;align-items:center;justify-content:center;height:100vh;gap:18px;background:#1e1e1e">
<div style="font-size:42px;font-weight:bold;color:#9b59b6">ENGINE VS ENGINE</div>
<div style="font-size:13px;color:#888">Choose a level for each side, then watch the bots play.</div>
<div style="display:flex;gap:40px;margin-top:8px">
<div>
<div style="font-size:14px;font-weight:bold;color:#fff;margin-bottom:8px;text-align:center">WHITE BOT</div>
<div id="eve-white-grid" style="display:grid;grid-template-columns:repeat(4,70px);gap:6px"></div>
</div>
<div>
<div style="font-size:14px;font-weight:bold;color:#fff;margin-bottom:8px;text-align:center">BLACK BOT</div>
<div id="eve-black-grid" style="display:grid;grid-template-columns:repeat(4,70px);gap:6px"></div>
</div>
</div>
<div id="eve-summary" style="font-size:18px;font-weight:bold;color:#f1c40f;margin-top:4px"></div>
<button class="btn win" style="font-size:18px;padding:12px 40px" onclick="startEve()">START</button>
<button class="btn" style="padding:10px 32px" onclick="goMenu()">BACK</button>
</div>
<!-- GAME -->
<div id="game">
<div id="board-area">
<div id="season-display"></div>
<div id="variant-badge" style="font-size:12px;color:#9b59b6;min-height:16px;margin-left:14px"></div>
<div id="board-and-ranks">
<div id="rank-labels"></div>
<canvas id="board" width="600" height="600"></canvas>
</div>
<div id="file-labels"></div>
<div id="opening-display"></div>
<div id="threecheck-display" style="font-size:12px;color:#f1c40f;min-height:16px;margin-left:14px;display:none"></div>
</div>
<div id="sidebar">
<div class="player-card">
<div class="player-label" id="top-label">BOT (Black)</div>
<div class="player-clock" id="top-clock">10:00</div>
<div class="player-captures" id="top-captures"></div>
</div>
<div class="move-history-box">
<div class="move-history-title">MOVE HISTORY</div>
<div id="move-history-text"></div>
</div>
<div class="sidebar-btn-group">
<button class="btn" onclick="goMenu()">HOME MENU</button>
<button class="btn accent" id="hint-btn" onclick="useHint()">HINT (3 left)</button>
<button class="btn accent" onclick="downloadPGN()">DOWNLOAD PGN</button>
<button class="btn" id="takeback-btn" onclick="doTakeback()">TAKEBACK (8 left)</button>
<div style="display:flex;gap:6px" id="resign-draw-row">
<button class="btn loss" style="flex:1" id="resign-btn" onclick="doResign()">RESIGN</button>
<button class="btn accent" style="flex:1" id="draw-btn" onclick="offerDraw()">DRAW?</button>
</div>
</div>
<div class="player-card">
<div class="player-label" id="btm-label">YOU (White)</div>
<div class="player-clock" id="btm-clock">10:00</div>
<div class="player-captures" id="btm-captures"></div>
</div>
</div>
</div>
<!-- PUZZLE -->
<div id="puzzle">
<div id="board-area-puzzle">
<div id="board-and-ranks-puzzle">
<div id="rank-labels-puzzle"></div>
<canvas id="board-puzzle" width="600" height="600"></canvas>
</div>
<div id="file-labels-puzzle"></div>
</div>
<div id="puzzle-sidebar">
<div style="text-align:center;font-size:17px;font-weight:bold;color:#f1c40f;margin-bottom:4px">LICHESS PUZZLE</div>
<div class="puzzle-info-card">
<div class="puz-lbl" id="puz-turn">Loading...</div>
<div class="puz-sub" id="puz-progress">Progress: 0/0</div>
<div class="puz-tiny" id="puz-diff" style="color:#f1c40f">Difficulty: NORMAL</div>
<div class="puz-tiny" id="puz-rating">Rating: ?</div>
<div class="puz-timer" id="puz-timer" style="color:#f1c40f">Time: 0s</div>
</div>
<div id="puzzle-status-banner"></div>
<div id="puzzle-feedback"></div>
<div class="sidebar-btn-group">
<button class="btn" onclick="fetchNewPuzzle()">NEW PUZZLE</button>
<button class="btn" onclick="goMenu()">BACK TO MENU</button>
</div>
</div>
</div>
<!-- Promotion overlay -->
<div class="overlay" id="promo-overlay" style="display:none">
<div class="overlay-title">PROMOTE</div>
<div class="promo-row" id="promo-row"></div>
</div>
<!-- Settings overlay -->
<div class="overlay" id="settings-overlay" style="display:none;z-index:60">
<div style="background:#262421;border-radius:15px;padding:30px 40px;min-width:400px">
<div style="font-size:22px;font-weight:bold;color:#f1c40f;text-align:center;margin-bottom:24px">SETTINGS</div>
<div id="settings-rows"></div>
<div style="text-align:center;margin-top:24px">
<button class="btn" style="padding:10px 36px" onclick="closeSettings()">CLOSE</button>
</div>
</div>
</div>
<!-- Game-over overlay -->
<div class="overlay" id="gameover-overlay" style="display:none">
<div class="overlay-title" id="gameover-msg">CHECKMATE!</div>
<button class="btn win" style="font-size:16px;padding:12px 28px" onclick="goMenu()">BACK TO MENU</button>
<button class="btn accent" style="font-size:16px;padding:12px 28px" onclick="rematch()">REMATCH</button>
<button class="btn" style="font-size:16px;padding:12px 28px" onclick="downloadPGN()">DOWNLOAD PGN</button>
</div>
<div id="toast"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js"></script>
<script>
// ==================== CONSTANTS ====================
const BOARD_SIZE = 600;
const SQ = 75;
const LEVEL_CONFIG = { 1:[1,0.85,500,250],2:[2,0.60,700,500],3:[3,0.35,900,800],4:[4,0.15,1100,1100],5:[5,0.05,1300,1450],6:[7,0.0,1600,1750],7:[10,0.0,1800,2200],8:[15,0.0,2000,2850] };
const TAKEBACK_LIMITS = {1:8,2:8,3:4,4:4,5:2,6:2,7:1,8:1};
const THEMES = [["#eeeed2","#769656"],["#dee3e6","#8ca2ad"],["#ebecd0","#ba5546"]];
const SEASONAL_THEMES = { 1:["#f0f0f0","#c0a060","New Year"],2:["#ffe4e1","#c0394b","Valentine's"],3:["#d4edda","#2d7a3a","St. Patrick's"],4:["#fde8f0","#d4789a","Spring"],5:["#fffde7","#f9a825","May Flowers"],6:["#e3f2fd","#1565c0","Summer"],7:["#ffebee","#b71c1c","Independence"],8:["#e0f7fa","#00838f","Ocean"],9:["#fff3e0","#e65100","Autumn"],10:["#1a1a1a","#ff6600","Halloween"],11:["#fff8e1","#795548","Thanksgiving"],12:["#ffebee","#1b5e20","Christmas"] };
const SYMBOLS = { P:'♙',N:'♘',B:'♗',R:'♖',Q:'♕',K:'♔', p:'♟',n:'♞',b:'♝',r:'♜',q:'♛',k:'♚' };
const ECO_OPENINGS = { "e2e4":"King's Pawn Opening","e2e4 e7e5":"Open Game","e2e4 e7e5 g1f3":"King's Knight Opening","e2e4 e7e5 g1f3 b8c6":"Three Knights","e2e4 e7e5 g1f3 b8c6 f1b5":"Ruy Lopez","e2e4 e7e5 g1f3 b8c6 f1c4":"Italian Game","e2e4 e7e5 g1f3 b8c6 f1c4 g8f6":"Two Knights Defense","e2e4 e7e5 g1f3 b8c6 d2d4":"Scotch Game","e2e4 e7e5 g1f3 b8c6 d2d4 e5d4":"Scotch Game","e2e4 e7e5 g1f3 g8f6":"Petrov's Defense","e2e4 e7e5 f2f4":"King's Gambit","e2e4 e7e5 f2f4 e5f4":"King's Gambit Accepted","e2e4 e7e5 f1c4":"Bishop's Opening","e2e4 c7c5":"Sicilian Defense","e2e4 c7c5 g1f3":"Sicilian Defense","e2e4 c7c5 g1f3 d7d6":"Sicilian Defense","e2e4 c7c5 g1f3 d7d6 d2d4 c5d4 f3d4 g8f6 b1c3 a7a6":"Sicilian Najdorf","e2e4 c7c5 g1f3 b8c6":"Sicilian Classical","e2e4 c7c5 g1f3 e7e6":"Sicilian Scheveningen","e2e4 c7c5 b1c3":"Sicilian Closed","e2e4 e7e6":"French Defense","e2e4 e7e6 d2d4 d7d5":"French Defense","e2e4 e7e6 d2d4 d7d5 b1c3":"French Classical","e2e4 e7e6 d2d4 d7d5 e4e5":"French Advance","e2e4 c7c6":"Caro-Kann Defense","e2e4 c7c6 d2d4 d7d5":"Caro-Kann Defense","e2e4 c7c6 d2d4 d7d5 b1c3":"Caro-Kann Classical","e2e4 c7c6 d2d4 d7d5 e4e5":"Caro-Kann Advance","e2e4 d7d5":"Scandinavian Defense","e2e4 d7d5 e4d5":"Scandinavian Defense","e2e4 g7g6":"Modern Defense","e2e4 d7d6":"Pirc Defense","e2e4 d7d6 d2d4 g8f6":"Pirc Defense","e2e4 g8f6":"Alekhine's Defense","e2e4 b7b6":"Owen's Defense","d2d4":"Queen's Pawn Opening","d2d4 d7d5":"Queen's Pawn Game","d2d4 d7d5 c2c4":"Queen's Gambit","d2d4 d7d5 c2c4 e7e6":"Queen's Gambit Declined","d2d4 d7d5 c2c4 d5c4":"Queen's Gambit Accepted","d2d4 d7d5 c2c4 c7c6":"Slav Defense","d2d4 d7d5 c2c4 c7c6 g1f3 g8f6":"Slav Defense","d2d4 g8f6":"Indian Defense","d2d4 g8f6 c2c4":"Indian Defense","d2d4 g8f6 c2c4 g7g6":"King's Indian Defense","d2d4 g8f6 c2c4 g7g6 b1c3":"King's Indian Defense","d2d4 g8f6 c2c4 e7e6 b1c3 f8b4":"Nimzo-Indian Defense","d2d4 g8f6 c2c4 e7e6 g1f3 b7b6":"Queen's Indian Defense","d2d4 g8f6 c2c4 c7c5":"Benoni Defense","d2d4 g8f6 c2c4 c7c5 d4d5":"Benoni Defense","d2d4 f7f5":"Dutch Defense","d2d4 f7f5 c2c4":"Dutch Defense","c2c4":"English Opening","c2c4 e7e5":"English Opening","c2c4 g8f6":"English Opening","g1f3":"Reti Opening","g1f3 d7d5":"Reti Opening","g1f3 d7d5 c2c4":"Reti Opening","g2g3":"King's Fianchetto","b2b3":"Larsen's Opening","b2b4":"Polish Opening","f2f4":"Bird's Opening" };
const PUZZLE_DIFFICULTIES = ["easiest","easier","normal","harder","hardest"];
const VARIANTS = { standard:{name:"Standard",desc:"Classic chess"}, blindfold:{name:"Blindfold",desc:"Pieces are hidden — play from memory"}, koth:{name:"King of the Hill",desc:"First king to centre wins"}, threecheck:{name:"Three-check",desc:"Win by giving 3 checks"}, atomic:{name:"Atomic",desc:"Captures explode nearby pieces (kings explode!)"}, antichess:{name:"Antichess",desc:"Lose all your pieces to win"}, crazyhouse:{name:"Crazyhouse",desc:"Drop captured pieces back on the board"}, chess960:{name:"Chess960",desc:"Fischer Random — randomised back rank, no castling"}, fog:{name:"Fog of War",desc:"Only see squares your pieces can reach"}, racingkings:{name:"Racing Kings",desc:"Race your king to rank 8"}, horde:{name:"Horde",desc:"White pawns swarm vs black pieces"} };
const VARIANT_KEYS = Object.keys(VARIANTS);
const KOTH_CENTRE = ["d4","d5","e4","e5"];
const PUZZLE_FAST_THRESHOLD = 20;
const PUZZLE_SLOW_THRESHOLD = 60;
// ==================== GLOBAL STATE ====================
let state = "MENU", mode = "PRESET", userColor = 'w';
let currentLevel = 1, customElo = 1500, timerSetting = 600, incrementSetting = 0;
let resigned = false, drawOfferPending = false;
let currentThemeIdx = 0, useSeasonal = true;
const currentMonth = new Date().getMonth() + 1;
let chess = new Chess();
let selectedSq = null, legalDots = [], capturedByWhite = [], capturedByBlack = [];
let moveHistory = [], boardHistory = [];
let whiteTime = 600, blackTime = 600;
let hintsUsed = 0, hintMove = null, pendingPromotion = null;
let botThinking = false, localMultiplayer = false;
let currentOpening = "", takebacksUsed = 0;
let timerInterval = null, lastTick = null;
let puzzleChess = null, puzzleSolution = [], puzzleMoveIdx = 0, puzzleStatus = "";
let puzzleSelectedSq = null, puzzleLegalDots = [];
let puzzleRating = "", puzzleThemesStr = "", puzzleDifficulty = "normal";
let puzzlePlayerColor = 'w', puzzleAdaptiveIdx = 2;
let puzzleStartTime = null, puzzleLastFeedback = "";
let puzzleTimerInterval = null;
let currentVariant = "standard";
let checksByWhite = 0, checksByBlack = 0;
let eveMode = false, eveWhiteLevel = 1, eveBlackLevel = 8;
let eveInterval = null;
let czhPocket = {w:[],b:[]};
let czhSelectedPocket = null;
let czhDragPiece = null;
// Settings — persisted in localStorage
const SETTINGS_KEY = 'omnichess_settings';
function loadSettings() { try { const s = JSON.parse(localStorage.getItem(SETTINGS_KEY)); return Object.assign({moveSounds:true,captureSounds:true,showDots:true,checkSounds:true}, s||{}); } catch(e) { return {moveSounds:true,captureSounds:true,showDots:true,checkSounds:true}; } }
function saveSettings() { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch(e) {} }
let settings = loadSettings();
let sf = null, sfReady = false, pendingBotCb = null, pendingHintCb = null;
// ==================== STOCKFISH & UTILITIES ====================
function attachSfHandlers(w) { w.onmessage = e => { const l = typeof e.data === 'string' ? e.data : ''; if (!l) return; if (l === 'uciok') { w.postMessage('isready'); return; } if (l === 'readyok') { sfReady = true; return; } if (l.startsWith('bestmove')) { const mv = l.split(' ')[1]; if (mv && mv !== '(none)') { if (pendingHintCb) { pendingHintCb(mv); pendingHintCb = null; } else if (pendingBotCb) { pendingBotCb(mv); pendingBotCb = null; } } } }; w.onerror = () => { sf = null; sfReady = false; }; }
function initStockfish() { const SF18 = 'https://unpkg.com/stockfish@18.0.7/src/stockfish-18-lite-single.js'; fetch(SF18).then(r => { if (!r.ok) throw new Error(); return r.blob(); }).then(blob => { sf = new Worker(URL.createObjectURL(blob)); attachSfHandlers(sf); sf.postMessage('uci'); }).catch(() => { try { const blob = new Blob(["importScripts('" + SF18 + "');"], { type: 'application/javascript' }); sf = new Worker(URL.createObjectURL(blob)); attachSfHandlers(sf); sf.postMessage('uci'); } catch(e) { console.warn('Stockfish 18 unavailable'); } }); }
function getBotMove(cb) { if (currentVariant === 'antichess') { cb(randomUCI()); return; } if (currentVariant === 'horde') { cb(randomHordeUCI()); return; } if (!sf || !sfReady) { cb(randomUCI()); return; } const [depth, rChance, thinkMs, targetElo] = LEVEL_CONFIG[currentLevel]; if (Math.random() < rChance) { cb(randomUCI()); return; } if (mode === "ELO") { sf.postMessage('setoption name UCI_LimitStrength value true'); sf.postMessage(`setoption name UCI_Elo value ${Math.max(1320, Math.min(3190, customElo))}`); } else { sf.postMessage('setoption name UCI_LimitStrength value true'); sf.postMessage(`setoption name UCI_Elo value ${Math.max(100, Math.min(3190, targetElo))}`); } sf.postMessage(`position fen ${chess.fen()}`); sf.postMessage(`go movetime ${thinkMs}`); pendingBotCb = cb; }
function getBotMoveForLevel(level, cb) { if (currentVariant === 'antichess' || currentVariant === 'atomic') { cb(randomUCI()); return; } if (currentVariant === 'horde') { cb(randomHordeUCI()); return; } if (!sf || !sfReady) { cb(randomUCI()); return; } const [depth, rChance, thinkMs, targetElo] = LEVEL_CONFIG[level]; if (Math.random() < rChance) { cb(randomUCI()); return; } sf.postMessage('setoption name UCI_LimitStrength value true'); sf.postMessage(`setoption name UCI_Elo value ${Math.max(100, Math.min(3190, targetElo))}`); sf.postMessage(`position fen ${chess.fen()}`); sf.postMessage(`go movetime ${thinkMs}`); pendingBotCb = cb; }
function randomUCI() { const mvs = chess.moves({ verbose: true }); if (!mvs.length) return null; const m = mvs[Math.floor(Math.random() * mvs.length)]; return m.from + m.to + (m.promotion || ''); }
function executeCzhDrop(pieceType, toSq) {
const color = chess.turn();
const pocket = czhPocket[color];
const idx = pocket.indexOf(pieceType);
if (idx === -1) return false;
if (chess.get(toSq)) return false;
const files = 'abcdefgh';
const pieceChar = color === 'w' ? pieceType.toUpperCase() : pieceType.toLowerCase();
let rows = [];
for (let r = 0; r < 8; r++) {
let row = ''; let empty = 0;
for (let f = 0; f < 8; f++) {
const sq = files[f] + (8 - r);
const p = chess.get(sq);
if (sq === toSq) { if (empty > 0) { row += empty; empty = 0; } row += pieceChar; }
else if (p) { if (empty > 0) { row += empty; empty = 0; } row += p.color === 'w' ? p.type.toUpperCase() : p.type.toLowerCase(); }
else { empty++; }
}
if (empty > 0) row += empty;
rows.push(row);
}
const fenParts = chess.fen().split(' ');
fenParts[0] = rows.join('/'); fenParts[1] = color === 'w' ? 'b' : 'w'; fenParts[3] = '-';
chess = new Chess(fenParts.join(' '));
pocket.splice(idx, 1);
moveHistory.push('@' + toSq);
playMoveSound(false);
czhSelectedPocket = null; selectedSq = null; legalDots = [];
if (incrementSetting > 0) { if (chess.turn() === 'b') whiteTime += incrementSetting; else blackTime += incrementSetting; }
return true;
}
function playBeep(freq, dur) { try { if (!window.audioCtx) window.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const o = window.audioCtx.createOscillator(), g = window.audioCtx.createGain(); o.connect(g); g.connect(window.audioCtx.destination); o.type = 'square'; o.frequency.value = freq; g.gain.setValueAtTime(0.15, window.audioCtx.currentTime); g.gain.exponentialRampToValueAtTime(0.001, window.audioCtx.currentTime + dur); o.start(window.audioCtx.currentTime); o.stop(window.audioCtx.currentTime + dur); } catch(e) {} }
function playMoveSound(isCap) { if (isCap) { if (settings.captureSounds) playBeep(880, 0.15); } else { if (settings.moveSounds) playBeep(440, 0.1); } }
// ==================== ATOMIC EXPLOSION — KINGS EXPLODE! ====================
function atomicExplode(board, explosionSquare) {
// board is a Chess.js instance, explosionSquare is the square where capture happened
const files = 'abcdefgh';
const expFile = explosionSquare.charCodeAt(0) - 97;
const expRank = parseInt(explosionSquare[1]);
// Get current board array
const boardArray = board.board();
// Create mutable representation: map square -> piece object
let pieceMap = new Map();
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const piece = boardArray[r][f];
if (piece) {
const sq = files[f] + (8 - r);
pieceMap.set(sq, { type: piece.type, color: piece.color });
}
}
}
// Remove the explosion square (capturing piece and captured piece)
pieceMap.delete(explosionSquare);
// Remove all pieces in the 3x3 area (including kings!)
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (df === 0 && dr === 0) continue;
const nf = expFile + df;
const nr = expRank + dr;
if (nf >= 0 && nf < 8 && nr >= 1 && nr <= 8) {
const adjSq = files[nf] + nr;
pieceMap.delete(adjSq);
}
}
}
// Build new board array and FEN
let newRows = [];
for (let r = 0; r < 8; r++) {
let row = '';
let empty = 0;
for (let f = 0; f < 8; f++) {
const sq = files[f] + (8 - r);
const piece = pieceMap.get(sq);
if (!piece) {
empty++;
} else {
if (empty > 0) {
row += empty;
empty = 0;
}
const symbol = piece.color === 'w' ? piece.type.toUpperCase() : piece.type.toLowerCase();
row += symbol;
}
}
if (empty > 0) row += empty;
newRows.push(row);
}
const newFen = newRows.join('/') + ' ' + board.fen().split(' ').slice(1).join(' ');
return newFen;
}
function executeMoveAtomic(from, to, promo) {
const cap = chess.get(to);
const movingPiece = chess.get(from);
const isEP = movingPiece?.type === 'p' && !cap && from[0] !== to[0];
const isCapture = !!(cap || isEP);
playMoveSound(isCapture);
// Save snapshot
boardHistory.push({
fen: chess.fen(),
capW: [...capturedByWhite],
capB: [...capturedByBlack],
hist: [...moveHistory]
});
// Execute the move
const result = chess.move({ from, to, promotion: promo || 'q' });
if (!result) {
boardHistory.pop();
return false;
}
moveHistory.push(result.san);
hintMove = null;
// Apply increment
if (incrementSetting > 0) { if (chess.turn() === 'b') whiteTime += incrementSetting; else blackTime += incrementSetting; }
if (cap) {
if (result.color === 'w') capturedByWhite.push(cap.type);
else capturedByBlack.push(cap.type.toUpperCase());
}
if (result.flags.includes('e')) {
if (result.color === 'w') capturedByWhite.push('p');
else capturedByBlack.push('P');
}
// If this was a capture, apply explosion
if (isCapture || result.flags.includes('c')) {
const explodedFen = atomicExplode(chess, to);
chess = new Chess(explodedFen);
}
return true;
}
// ==================== THEME HELPERS ====================
function getActiveTheme() { if (useSeasonal && SEASONAL_THEMES[currentMonth]) return [SEASONAL_THEMES[currentMonth][0], SEASONAL_THEMES[currentMonth][1]]; return THEMES[currentThemeIdx]; }
function getSeasonalName() { if (useSeasonal && SEASONAL_THEMES[currentMonth]) return SEASONAL_THEMES[currentMonth][2]; return ""; }
function getThemeDisplayName() { const n = getSeasonalName(); return n || `Theme ${currentThemeIdx + 1}`; }
function getTakebackLimit() { if (mode === "ELO") return 0; return TAKEBACK_LIMITS[currentLevel] || 1; }
// ==================== VARIANTS & WIN CONDITIONS ====================
function cycleVariant() { const idx = VARIANT_KEYS.indexOf(currentVariant); currentVariant = VARIANT_KEYS[(idx + 1) % VARIANT_KEYS.length]; buildMenu(); }
function checkVariantWin() {
if (currentVariant === 'horde') { return hCheckWin(chess.fen()); }
if (currentVariant === 'antichess') {
if (chess.game_over()) return chess.turn() === 'w' ? 'White wins! (Antichess — no moves)' : 'Black wins! (Antichess — no moves)';
const moves = chess.moves(); if (moves.length === 0) return chess.turn() === 'w' ? 'White wins! (Antichess)' : 'Black wins! (Antichess)';
return null;
}
if (currentVariant === 'atomic') {
// Check if either king is missing (destroyed by explosion)
const boardArr = chess.board();
let wk = false, bk = false;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = boardArr[r][f];
if (p && p.type === 'k') {
if (p.color === 'w') wk = true;
else bk = true;
}
}
}
if (!wk) return 'Black wins! (Atomic — white king exploded)';
if (!bk) return 'White wins! (Atomic — black king exploded)';
if (chess.game_over()) {
if (chess.in_checkmate()) return chess.turn() === 'b' ? 'White wins! (Atomic)' : 'Black wins! (Atomic)';
return 'DRAW (Atomic)';
}
return null;
}
if (currentVariant === 'crazyhouse') {
const boardArr = chess.board();
let wk = false, bk = false;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = boardArr[r][f];
if (p && p.type === 'k') {
if (p.color === 'w') wk = true;
else bk = true;
}
}
}
if (!wk) return 'Black wins! (Crazyhouse)';
if (!bk) return 'White wins! (Crazyhouse)';
if (chess.game_over()) {
if (chess.in_checkmate()) return chess.turn() === 'b' ? 'White wins! (Crazyhouse)' : 'Black wins! (Crazyhouse)';
return 'DRAW (Crazyhouse)';
}
return null;
}
if (chess.game_over()) { if (chess.in_checkmate()) return chess.turn() === 'b' ? 'White wins!' : 'Black wins!'; return 'DRAW'; }
if (currentVariant === 'koth') {
const files = 'abcdefgh';
let wkSq = null, bkSq = null;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = chess.board()[r][f];
if (p && p.type === 'k') {
const sq = files[f] + (8 - r);
if (p.color === 'w') wkSq = sq;
else bkSq = sq;
}
}
}
if (wkSq && KOTH_CENTRE.includes(wkSq)) return 'White wins! (King of the Hill)';
if (bkSq && KOTH_CENTRE.includes(bkSq)) return 'Black wins! (King of the Hill)';
}
if (currentVariant === 'threecheck') { if (checksByWhite >= 3) return 'White wins! (Three-check)'; if (checksByBlack >= 3) return 'Black wins! (Three-check)'; }
return null;
}
function updateVariantUI() { const v = VARIANTS[currentVariant]; document.getElementById('variant-badge').textContent = currentVariant !== 'standard' ? `Variant: ${v.name}` : ''; const tcd = document.getElementById('threecheck-display'); if (currentVariant === 'threecheck') { tcd.style.display = 'block'; tcd.textContent = `Checks — White: ${checksByWhite}/3 Black: ${checksByBlack}/3`; } else { tcd.style.display = 'none'; } }
// ==================== HORDE ENGINE ====================
const HF='abcdefgh';
function hFen2Bd(fen){const rows=fen.split(' ')[0].split('/'),bd={};for(let r=0;r<8;r++){let f=0;for(const c of rows[r]){if(c>='1'&&c<='8')f+=+c;else{bd[HF[f]+(8-r)]={type:c.toLowerCase(),color:c===c.toUpperCase()?'w':'b'};f++;}}}return bd;}
function hBd2Fen(bd){const rows=[];for(let rk=8;rk>=1;rk--){let row='',e=0;for(const fl of HF){const p=bd[fl+rk];if(p){if(e){row+=e;e=0;}row+=p.color==='w'?p.type.toUpperCase():p.type;}else e++;}if(e)row+=e;rows.push(row);}return rows.join('/');}
function hAtk(bd,sq,by){const sf=sq.charCodeAt(0)-97,sr=+sq[1];for(const[s,p]of Object.entries(bd)){if(!p||p.color!==by)continue;const af=s.charCodeAt(0)-97,ar=+s[1],df=sf-af,dr=sr-ar;if(p.type==='p'){const d=by==='w'?1:-1;if(dr===d&&Math.abs(df)===1)return true;}else if(p.type==='n'){if((Math.abs(df)===2&&Math.abs(dr)===1)||(Math.abs(df)===1&&Math.abs(dr)===2))return true;}else if(p.type==='k'){if(Math.abs(df)<=1&&Math.abs(dr)<=1&&(df||dr))return true;}else if('brq'.includes(p.type)){const dg=Math.abs(df)===Math.abs(dr)&&df!==0,st=(df===0)!==(dr===0);const ok=(p.type==='b'&&dg)||(p.type==='r'&&st)||(p.type==='q'&&(dg||st));if(ok){const sv=df?df/Math.abs(df):0,rv=dr?dr/Math.abs(dr):0;let cf=af+sv,cr=ar+rv;let clear=true;while(cf!==sf||cr!==sr){if(bd[HF[cf]+cr]){clear=false;break;}cf+=sv;cr+=rv;}if(clear)return true;}}}return false;}
function hApplyBd(bd,from,to,promo,ep){const nb={...bd},p=nb[from];delete nb[from];if(p.type==='p'&&to===ep)delete nb[to[0]+(p.color==='w'?+to[1]-1:+to[1]+1)];if(p.type==='k'&&Math.abs(to.charCodeAt(0)-from.charCodeAt(0))===2){const rk=from[1],ks=to[0]>from[0];nb[(ks?'f':'d')+rk]=nb[(ks?'h':'a')+rk];delete nb[(ks?'h':'a')+rk];}const tr=+to[1];nb[to]=(p.type==='p'&&(tr===8||tr===1))?{type:promo||'q',color:p.color}:p;return nb;}
function hMoves(fen,sq){const pts=fen.split(' '),turn=pts[1],cast=pts[2],ep=pts[3]!=='-'?pts[3]:null,bd=hFen2Bd(fen),p=bd[sq];if(!p||p.color!==turn)return[];const sf=sq.charCodeAt(0)-97,sr=+sq[1],raw=[];if(p.type==='p'){const dir=turn==='w'?1:-1,stR=turn==='w'?2:7,fwd=HF[sf]+(sr+dir);if(sr+dir>=1&&sr+dir<=8&&!bd[fwd]){raw.push(fwd);if(sr===stR){const f2=HF[sf]+(sr+2*dir);if(!bd[f2])raw.push(f2);}}for(const d of[-1,1]){if(sf+d<0||sf+d>7)continue;const cs=HF[sf+d]+(sr+dir);if((bd[cs]&&bd[cs].color!==turn)||cs===ep)raw.push(cs);}}else if(p.type==='n'){for(const[df,dr]of[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]){const nf=sf+df,nr=sr+dr;if(nf>=0&&nf<=7&&nr>=1&&nr<=8){const ns=HF[nf]+nr;if(!bd[ns]||bd[ns].color!==turn)raw.push(ns);}}}else if(p.type==='k'){for(const[df,dr]of[[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]){const nf=sf+df,nr=sr+dr;if(nf>=0&&nf<=7&&nr>=1&&nr<=8){const ns=HF[nf]+nr;if(!bd[ns]||bd[ns].color!==turn)raw.push(ns);}}if(turn==='b'&&cast.includes('k')&&!bd.f8&&!bd.g8&&!hAtk(bd,'e8','w')&&!hAtk(bd,'f8','w')&&!hAtk(bd,'g8','w'))raw.push('g8');if(turn==='b'&&cast.includes('q')&&!bd.d8&&!bd.c8&&!bd.b8&&!hAtk(bd,'e8','w')&&!hAtk(bd,'d8','w')&&!hAtk(bd,'c8','w'))raw.push('c8');}else{const dirs=[];if('bq'.includes(p.type))dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);if('rq'.includes(p.type))dirs.push([-1,0],[1,0],[0,-1],[0,1]);for(const[df,dr]of dirs){let nf=sf+df,nr=sr+dr;while(nf>=0&&nf<=7&&nr>=1&&nr<=8){const ns=HF[nf]+nr;if(bd[ns]){if(bd[ns].color!==turn)raw.push(ns);break;}raw.push(ns);nf+=df;nr+=dr;}}}return raw.filter(to=>{const nb=hApplyBd(bd,sq,to,'q',ep);let kSq=null;for(const[s,pc]of Object.entries(nb)){if(pc&&pc.type==='k'&&pc.color===turn){kSq=s;break;}}return!kSq||!hAtk(nb,kSq,turn==='w'?'b':'w');});}
function hApplyFen(fen,from,to,promo){const pts=fen.split(' '),turn=pts[1],ep=pts[3]!=='-'?pts[3]:null,bd=hFen2Bd(fen),p=bd[from],nb=hApplyBd(bd,from,to,promo,ep);let cast=pts[2];if(p.type==='k')cast=cast.replace(turn==='w'?/[KQ]/g:/[kq]/g,'');if(p.type==='r'){if(from==='a1')cast=cast.replace('Q','');else if(from==='h1')cast=cast.replace('K','');else if(from==='a8')cast=cast.replace('q','');else if(from==='h8')cast=cast.replace('k','');}if(to==='a1')cast=cast.replace('Q','');if(to==='h1')cast=cast.replace('K','');if(to==='a8')cast=cast.replace('q','');if(to==='h8')cast=cast.replace('k','');if(!cast)cast='-';const fr=+from[1],tr=+to[1],newEP=(p.type==='p'&&Math.abs(tr-fr)===2)?HF[from.charCodeAt(0)-97]+((fr+tr)/2):'-';const isCap=!!bd[to]||(p.type==='p'&&to===ep);const hm=isCap||p.type==='p'?0:+pts[4]+1;const fm=turn==='b'?+pts[5]+1:+pts[5];return`${hBd2Fen(nb)} ${turn==='w'?'b':'w'} ${cast} ${newEP} ${hm} ${fm}`;}
function hCheckWin(fen){const bd=hFen2Bd(fen),turn=fen.split(' ')[1];if(!Object.values(bd).some(p=>p&&p.color==='w'))return'Black wins! (Horde)';const hasMv=Object.entries(bd).some(([s,p])=>p&&p.color===turn&&hMoves(fen,s).length>0);if(!hasMv){if(turn==='b'){let bk=null;for(const[s,p]of Object.entries(bd)){if(p&&p.type==='k'&&p.color==='b'){bk=s;break;}}return bk&&hAtk(bd,bk,'w')?'White wins! (Horde — checkmate)':'DRAW (Horde — stalemate)';}return'Black wins! (Horde — white has no moves)';}return null;}
function randomHordeUCI(){const fen=chess.fen(),bd=hFen2Bd(fen),turn=fen.split(' ')[1];const froms=Object.keys(bd).filter(s=>bd[s]&&bd[s].color===turn).sort(()=>Math.random()-.5);for(const f of froms){const mvs=hMoves(fen,f);if(mvs.length){const to=mvs[Math.floor(Math.random()*mvs.length)];const p=bd[f],tr=+to[1];return f+to+((p.type==='p'&&(tr===8||tr===1))?'q':'');}}return null;}
function executeHordeMove(from,to,promo){const fen=chess.fen(),bd=hFen2Bd(fen),cap=bd[to],p=bd[from],ep=fen.split(' ')[3]!=='-'?fen.split(' ')[3]:null,isEP=p&&p.type==='p'&&!cap&&from[0]!==to[0]&&to===ep;playMoveSound(!!(cap||isEP));boardHistory.push({fen,capW:[...capturedByWhite],capB:[...capturedByBlack],hist:[...moveHistory]});const newFen=hApplyFen(fen,from,to,promo||null);try{chess=new Chess(newFen);}catch(e){}moveHistory.push(from+(cap||isEP?'x':'')+to+(promo?'='+promo.toUpperCase():''));hintMove=null;if(incrementSetting>0){if(chess.turn()==='b')whiteTime+=incrementSetting;else blackTime+=incrementSetting;}if(cap){if(p.color==='w')capturedByWhite.push(cap.type);else capturedByBlack.push(cap.type.toUpperCase());}if(isEP){if(p.color==='w')capturedByWhite.push('p');else capturedByBlack.push('P');}updateSidebar();drawBoard();updateVariantUI();const vr=hCheckWin(newFen);if(vr){setTimeout(()=>showGameOver(vr),400);return;}if(!localMultiplayer&&chess.turn()!==userColor){botThinking=true;setTimeout(requestBotMove,150);}}
// ==================== MENU & NAVIGATION ====================
function buildMenu() { const g = document.getElementById('level-grid'); g.innerHTML = ''; for (let i = 1; i <= 8; i++) { const b = document.createElement('button'); b.className = 'btn' + (i === currentLevel && mode === 'PRESET' ? ' active' : ''); b.textContent = `Level ${i}`; b.onclick = () => { currentLevel = i; mode = 'PRESET'; buildMenu(); }; g.appendChild(b); } document.getElementById('elo-value').textContent = customElo; document.getElementById('elo-value').style.color = mode === 'ELO' ? '#f1c40f' : '#ffffff'; document.getElementById('timer-value').textContent = fmtTime(timerSetting); document.getElementById('btn-white').className = 'btn' + (userColor === 'w' ? ' active' : ''); document.getElementById('btn-black').className = 'btn' + (userColor === 'b' ? ' active' : ''); document.getElementById('theme-name-display').textContent = getThemeDisplayName(); const vv = VARIANTS[currentVariant]; document.getElementById('variant-name-display').textContent = vv.name; document.getElementById('variant-desc-display').textContent = vv.desc; }
function adjustElo(d) { customElo = Math.max(100, Math.min(3190, customElo + d)); mode = 'ELO'; buildMenu(); }
function setUserColor(c) { userColor = c; buildMenu(); }
function adjustTimer(d) { timerSetting = Math.max(10, timerSetting + d); buildMenu(); }
function adjustIncrement(d) { incrementSetting = Math.max(0, incrementSetting + d); document.getElementById('increment-value').textContent = `+${incrementSetting}s/move`; }
document.addEventListener('keydown', e => { if (state === 'MENU') { if (e.key === 't' || e.key === 'T') { useSeasonal = false; currentThemeIdx = (currentThemeIdx + 1) % THEMES.length; buildMenu(); } if (e.key === 's' || e.key === 'S') { useSeasonal = !useSeasonal; buildMenu(); } } if (state === 'PUZZLE' && e.key === 'Escape') goMenu(); });
function generateChess960FEN() {
const rank = new Array(8).fill(null);
rank[[0,2,4,6][Math.floor(Math.random()*4)]] = 'B';
rank[[1,3,5,7][Math.floor(Math.random()*4)]] = 'B';
let empty = rank.map((v,i) => v===null ? i : -1).filter(i => i>=0);
rank[empty.splice(Math.floor(Math.random()*empty.length),1)[0]] = 'Q';
rank[empty.splice(Math.floor(Math.random()*empty.length),1)[0]] = 'N';
rank[empty.splice(Math.floor(Math.random()*empty.length),1)[0]] = 'N';
rank[empty[0]]='R'; rank[empty[1]]='K'; rank[empty[2]]='R';
const w = rank.join(''), b = w.toLowerCase();
return `${b}/pppppppp/8/8/8/8/PPPPPPPP/${w} w - - 0 1`;
}
function resetGame() { const _vfen={'racingkings':'8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1','horde':'rnbqkbnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPP w kq - 0 1'}; chess = currentVariant === 'chess960' ? new Chess(generateChess960FEN()) : (_vfen[currentVariant] ? new Chess(_vfen[currentVariant]) : new Chess()); selectedSq = null; legalDots = []; capturedByWhite = []; capturedByBlack = []; moveHistory = []; boardHistory = []; whiteTime = timerSetting; blackTime = timerSetting; hintsUsed = 0; hintMove = null; pendingPromotion = null; botThinking = false; takebacksUsed = 0; currentOpening = ""; pendingBotCb = null; pendingHintCb = null; checksByWhite = 0; checksByBlack = 0; eveMode = false; clearInterval(eveInterval); resigned = false; drawOfferPending = false; czhPocket = {w:[],b:[]}; czhSelectedPocket = null; }
function show(id) { ['menu','game','puzzle','eve-setup'].forEach(s => { document.getElementById(s).style.display = s === id ? 'flex' : 'none'; }); }
function startGame(multi) { localMultiplayer = multi; resetGame(); state = 'PLAYING'; show('game'); document.getElementById('gameover-overlay').style.display = 'none'; document.getElementById('promo-overlay').style.display = 'none'; buildCoords(false); updateSidebar(); updateVariantUI(); drawBoard(); startTimer(); if (!localMultiplayer && chess.turn() !== userColor) { botThinking = true; setTimeout(requestBotMove, 300); } }
function enterPuzzle() { state = 'PUZZLE'; show('puzzle'); buildCoords(true); fetchNewPuzzle(); }
function goMenu() { clearInterval(timerInterval); clearInterval(puzzleTimerInterval); clearInterval(eveInterval); pendingBotCb = null; pendingHintCb = null; botThinking = false; eveMode = false; state = 'MENU'; show('menu'); document.getElementById('gameover-overlay').style.display = 'none'; document.getElementById('promo-overlay').style.display = 'none'; buildMenu(); }
function rematch() { document.getElementById('gameover-overlay').style.display = 'none'; if (eveMode) { const w = eveWhiteLevel, b = eveBlackLevel; resetGame(); eveWhiteLevel = w; eveBlackLevel = b; eveMode = true; localMultiplayer = false; userColor = 'w'; state = 'PLAYING'; show('game'); document.getElementById('gameover-overlay').style.display = 'none'; buildCoords(false); updateSidebar(); updateVariantUI(); drawBoard(); startTimer(); scheduleEveMove(); } else { startGame(localMultiplayer); } }
function enterEveSetup() { state = 'EVE_SETUP'; show('eve-setup'); buildEveSetup(); }
function buildEveSetup() {
['white','black'].forEach(side => {
const g = document.getElementById(`eve-${side}-grid`); g.innerHTML = '';
const cur = side === 'white' ? eveWhiteLevel : eveBlackLevel;
for (let i = 1; i <= 8; i++) {
const b = document.createElement('button');
b.className = 'btn' + (i === cur ? ' active' : '');
b.textContent = `Lv ${i}`; b.style.padding = '8px 0'; b.style.fontSize = '14px';
b.onclick = () => { if (side === 'white') eveWhiteLevel = i; else eveBlackLevel = i; buildEveSetup(); };
g.appendChild(b);
}
});
document.getElementById('eve-summary').textContent = `White: Level ${eveWhiteLevel} vs Black: Level ${eveBlackLevel}`;
}
function startEve() { resetGame(); eveMode = true; localMultiplayer = false; userColor = 'w'; state = 'PLAYING'; show('game'); document.getElementById('gameover-overlay').style.display = 'none'; document.getElementById('promo-overlay').style.display = 'none'; buildCoords(false); updateSidebar(); updateVariantUI(); drawBoard(); startTimer(); scheduleEveMove(); }
function scheduleEveMove() {
clearInterval(eveInterval);
eveInterval = setTimeout(() => {
if (state !== 'PLAYING' || !eveMode) return;
if (document.getElementById('gameover-overlay').style.display !== 'none') return;
const level = chess.turn() === 'w' ? eveWhiteLevel : eveBlackLevel;
getBotMoveForLevel(level, uci => {
if (!uci || state !== 'PLAYING' || !eveMode) return;
if (currentVariant === 'atomic') executeMoveAtomic(uci.slice(0,2), uci.slice(2,4), uci[4] || null);
else executeMove(uci.slice(0,2), uci.slice(2,4), uci[4] || null);
drawBoard(); updateSidebar(); updateVariantUI();
const vr = checkVariantWin();
if (vr) { setTimeout(() => showGameOver(vr), 400); return; }
scheduleEveMove();
});
}, 350);
}
function startTimer() { clearInterval(timerInterval); lastTick = Date.now(); timerInterval = setInterval(() => { if (state !== 'PLAYING') return; if (document.getElementById('gameover-overlay').style.display !== 'none') return; const drawAgreed = (whiteTime === -2 || blackTime === -2); if (drawAgreed || resigned) return; const now = Date.now(), dt = (now - lastTick) / 1000; lastTick = now; if (chess.turn() === 'w') whiteTime = Math.max(0, whiteTime - dt); else blackTime = Math.max(0, blackTime - dt); updateSidebar(); if (whiteTime <= 0) showGameOver("Time — Black wins!"); else if (blackTime <= 0) showGameOver("Time — White wins!"); }, 100); }
function doResign() { if (eveMode) return; resigned = true; const winner = localMultiplayer ? (chess.turn() === 'w' ? 'Black wins! (Resigned)' : 'White wins! (Resigned)') : (userColor === 'w' ? 'Black wins! (Resigned)' : 'White wins! (Resigned)'); showGameOver(winner); }
function offerDraw() { if (eveMode || drawOfferPending) return; if (localMultiplayer) { drawOfferPending = true; updateSidebar(); return; } const botAccepts = moveHistory.length >= 20; if (botAccepts) { whiteTime = -2; showGameOver('Draw agreed!'); } else { showToast('Bot declines the draw.'); } }
function confirmDraw() { if (!drawOfferPending || !localMultiplayer) return; whiteTime = -2; showGameOver('Draw agreed!'); }
function fmtTime(s) { s = Math.max(0, s); const m = Math.floor(s / 60), sc = Math.floor(s % 60); return `${String(m).padStart(2,'0')}:${String(sc).padStart(2,'0')}`; }
// ==================== SIDEBAR & BOARD DRAWING ====================
function updateSidebar() {
const bl = mode === 'ELO' ? `BOT (${customElo})` : `BOT (Lvl ${currentLevel})`;
let tl, bl2, tt, bt, tc, bc;
if (eveMode) { tl = `BOT Lvl ${eveBlackLevel} (Black)`; bl2 = `BOT Lvl ${eveWhiteLevel} (White)`; tt = blackTime; bt = whiteTime; tc = capturedByBlack; bc = capturedByWhite; }
else if (localMultiplayer) { tl = 'PLAYER 2 (Black)'; bl2 = 'PLAYER 1 (White)'; tt = blackTime; bt = whiteTime; tc = capturedByBlack; bc = capturedByWhite; }
else if (userColor === 'w') { tl = `${bl} (Black)`; bl2 = 'YOU (White)'; tt = blackTime; bt = whiteTime; tc = capturedByBlack; bc = capturedByWhite; }
else { tl = `${bl} (White)`; bl2 = 'YOU (Black)'; tt = whiteTime; bt = blackTime; tc = capturedByWhite; bc = capturedByBlack; }
document.getElementById('top-label').textContent = tl; document.getElementById('btm-label').textContent = bl2;
document.getElementById('top-clock').textContent = fmtTime(tt); document.getElementById('btm-clock').textContent = fmtTime(bt);
document.getElementById('top-captures').innerHTML = tc.map(p => `<span style="color:${p === p.toUpperCase() ? '#ffffff' : '#111111'}">${SYMBOLS[p] || p}</span>`).join('');
document.getElementById('btm-captures').innerHTML = bc.map(p => `<span style="color:${p === p.toUpperCase() ? '#ffffff' : '#111111'}">${SYMBOLS[p] || p}</span>`).join('');
const hintBtn = document.getElementById('hint-btn'); hintBtn.textContent = `HINT (${3 - hintsUsed} left)`; hintBtn.className = hintsUsed < 3 ? 'btn accent' : 'btn loss'; hintBtn.disabled = hintsUsed >= 3 || eveMode;
const tbBtn = document.getElementById('takeback-btn');
if (eveMode) { tbBtn.textContent = 'WATCHING'; tbBtn.className = 'btn'; tbBtn.disabled = true; tbBtn.onclick = null; }
else if (localMultiplayer) { const turnLabel = chess.turn() === 'w' ? "WHITE'S TURN" : "BLACK'S TURN"; tbBtn.textContent = turnLabel; tbBtn.className = 'btn'; tbBtn.disabled = false; tbBtn.onclick = null; }
else { tbBtn.onclick = doTakeback; const tbLimit = getTakebackLimit(); const tbLeft = tbLimit - takebacksUsed; const canTakeback = tbLeft > 0 && boardHistory.length >= 2 && chess.turn() === userColor; tbBtn.textContent = `TAKEBACK (${tbLeft} left)`; tbBtn.className = canTakeback ? 'btn' : 'btn loss'; tbBtn.disabled = !canTakeback; }
// Resign / Draw row
const rdr = document.getElementById('resign-draw-row');
const drawBtn = document.getElementById('draw-btn');
const resignBtn = document.getElementById('resign-btn');
if (rdr) {
rdr.style.display = eveMode ? 'none' : 'flex';
if (drawBtn) { if (drawOfferPending && localMultiplayer) { drawBtn.textContent = 'CONFIRM DRAW'; drawBtn.onclick = confirmDraw; } else { drawBtn.textContent = drawOfferPending ? 'WAITING...' : 'DRAW?'; drawBtn.onclick = offerDraw; drawBtn.disabled = drawOfferPending && !localMultiplayer; } }
}
const mhEl = document.getElementById('move-history-text'); mhEl.innerHTML = ''; const start = Math.max(0, moveHistory.length - 14); for (let i = start; i < moveHistory.length; i++) { const span = document.createElement('span'); span.textContent = `${i + 1}. ${moveHistory[i]}`; span.style.fontSize = '13px'; mhEl.appendChild(span); }
if (currentVariant === 'crazyhouse') {
const myColor = localMultiplayer ? chess.turn() : userColor;
const myPocket = czhPocket[myColor] || [];
const oppPocket = czhPocket[myColor === 'w' ? 'b' : 'w'] || [];
const sym = (t, c) => SYMBOLS[c === 'w' ? t.toUpperCase() : t] || t;
if (myPocket.length) {
const pEl = document.getElementById('btm-captures');
pEl.appendChild(document.createTextNode(' '));
myPocket.forEach(t => {
const s = document.createElement('span');
s.textContent = sym(t, myColor);
s.draggable = true;
s.title = 'Drag to drop on board';
s.style.cssText = 'color:#9b59b6;font-size:22px;cursor:grab;display:inline-block';
s.addEventListener('dragstart', e => { e.dataTransfer.setData('czh-piece', t); czhDragPiece = t; });
s.addEventListener('dragend', () => { czhDragPiece = null; });
pEl.appendChild(s);
});
}
if (oppPocket.length) document.getElementById('top-captures').innerHTML += ' <span style="color:#9b59b6;font-size:13px">[' + oppPocket.map(t => sym(t, myColor === 'w' ? 'b' : 'w')).join('') + ']</span>';
}
document.getElementById('opening-display').textContent = currentOpening ? `Opening: ${currentOpening}` : ''; const sn = getSeasonalName(); document.getElementById('season-display').textContent = sn ? `Theme: ${sn}` : '';
}
function buildCoords(isPuzzle) { const persp = isPuzzle ? puzzlePlayerColor : userColor; const files = persp === 'w' ? 'abcdefgh' : 'hgfedcba'; const ranks = persp === 'w' ? ['8','7','6','5','4','3','2','1'] : ['1','2','3','4','5','6','7','8']; const fEl = document.getElementById(isPuzzle ? 'file-labels-puzzle' : 'file-labels'); fEl.innerHTML = ''; for (const f of files) { const d = document.createElement('div'); d.className = 'file-label'; d.textContent = f; fEl.appendChild(d); } const rEl = document.getElementById(isPuzzle ? 'rank-labels-puzzle' : 'rank-labels'); rEl.innerHTML = ''; for (const r of ranks) { const d = document.createElement('div'); d.className = 'rank-label'; d.textContent = r; rEl.appendChild(d); } }
const boardCvs = document.getElementById('board'), bCtx = boardCvs.getContext('2d'), puzCvs = document.getElementById('board-puzzle'), pCtx = puzCvs.getContext('2d');
function colRowToSq(col, row, persp) { const file = persp === 'w' ? col : 7 - col; const rank = persp === 'w' ? 7 - row : row; return String.fromCharCode(97 + file) + (rank + 1); }
function sqToColRow(sq, persp) { const file = sq.charCodeAt(0) - 97, rank = parseInt(sq[1]) - 1; return persp === 'w' ? [file, 7 - rank] : [7 - file, rank]; }
function getFogVisibleSquares(game, color) { const visible = new Set(); const rows = ['1','2','3','4','5','6','7','8']; const files = ['a','b','c','d','e','f','g','h']; for (const f of files) for (const r of rows) { const sq = f + r; const piece = game.get(sq); if (piece && piece.color === color) { visible.add(sq); const moves = game.moves({ square: sq, verbose: true }); for (const m of moves) visible.add(m.to); } } return visible; }
function drawBoardOnCtx(ctx, game, selSq, dots, hint, persp, fogColor) { const theme = getActiveTheme(); ctx.clearRect(0, 0, 600, 600); const fogVisible = fogColor ? getFogVisibleSquares(game, fogColor) : null; for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const sq = colRowToSq(col, row, persp); const x = col * SQ, y = row * SQ; const isLight = (row + col) % 2 === 0; let fill = isLight ? theme[0] : theme[1]; const piece = game.get(sq); if (piece && piece.type === 'k' && piece.color === game.turn() && game.in_check()) fill = '#e74c3c'; if (selSq === sq) fill = '#f7f769'; if (hint && (hint.from === sq || hint.to === sq)) fill = 'rgba(52,152,219,0.7)'; if (currentVariant === 'koth' && KOTH_CENTRE.includes(sq)) fill = isLight ? '#d4b896' : '#a07850'; const sqVisible = !fogVisible || fogVisible.has(sq); ctx.fillStyle = fill; ctx.fillRect(x, y, SQ, SQ); if (piece && currentVariant !== 'blindfold' && sqVisible) { const sym = SYMBOLS[piece.color === 'w' ? piece.type.toUpperCase() : piece.type]; ctx.font = `bold ${Math.floor(SQ * 0.72)}px "Segoe UI Symbol",serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.strokeStyle = piece.color === 'w' ? '#444444' : '#cccccc'; ctx.lineWidth = 1.5; ctx.fillStyle = piece.color === 'w' ? '#ffffff' : '#111111'; ctx.strokeText(sym, x + SQ / 2, y + SQ / 2 + 2); ctx.fillText(sym, x + SQ / 2, y + SQ / 2 + 2); } if (!sqVisible) { ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(x, y, SQ, SQ); } } } for (const dotSq of dots) { const [dc, dr] = sqToColRow(dotSq, persp); const cx = dc * SQ + SQ / 2, cy = dr * SQ + SQ / 2; const hasPiece = game.get(dotSq); ctx.beginPath(); if (hasPiece) { ctx.arc(cx, cy, SQ / 2 - 3, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(231,76,60,0.63)'; ctx.lineWidth = 4; ctx.stroke(); } else { ctx.arc(cx, cy, 10, 0, Math.PI * 2); ctx.fillStyle = 'rgba(0,0,0,0.31)'; ctx.fill(); } } }
function drawBoard() { const persp = localMultiplayer ? chess.turn() : userColor; const fogColor = (currentVariant === 'fog' && !eveMode) ? (localMultiplayer ? chess.turn() : userColor) : null; drawBoardOnCtx(bCtx, chess, selectedSq, settings.showDots ? legalDots : [], hintMove, persp, fogColor); }
function drawPuzzleBoard() { if (puzzleChess) drawBoardOnCtx(pCtx, puzzleChess, puzzleSelectedSq, puzzleLegalDots, null, puzzlePlayerColor); }
// ==================== MOVE EXECUTION & EVENT HANDLERS ====================
function executeMove(from, to, promo) {
const cap = chess.get(to); const movingPiece = chess.get(from); const isEP = movingPiece?.type === 'p' && !cap && from[0] !== to[0]; playMoveSound(!!(cap || isEP));
boardHistory.push({ fen: chess.fen(), capW: [...capturedByWhite], capB: [...capturedByBlack], hist: [...moveHistory] });
const result = chess.move({ from, to, promotion: promo || 'q' }); if (!result) { boardHistory.pop(); return; }
moveHistory.push(result.san); hintMove = null;
if (chess.in_check() && settings.checkSounds) playBeep(660, 0.12);
// Apply increment
if (incrementSetting > 0) { if (chess.turn() === 'b') whiteTime += incrementSetting; else blackTime += incrementSetting; }
if (cap) { if (result.color === 'w') { capturedByWhite.push(cap.type); if (currentVariant === 'crazyhouse') czhPocket['w'].push(cap.type); } else { capturedByBlack.push(cap.type.toUpperCase()); if (currentVariant === 'crazyhouse') czhPocket['b'].push(cap.type); } }
if (result.flags.includes('e')) { if (result.color === 'w') capturedByWhite.push('p'); else capturedByBlack.push('P'); }
const stack = chess.history({ verbose: true }).map(m => m.from + m.to); let found = ''; for (let i = stack.length; i >= 1; i--) { const k = stack.slice(0, i).join(' '); if (ECO_OPENINGS[k]) { found = ECO_OPENINGS[k]; break; } } currentOpening = found;
updateSidebar(); drawBoard();
if (currentVariant === 'threecheck' && chess.in_check()) { if (chess.turn() === 'b') checksByWhite++; else checksByBlack++; }
updateVariantUI();
const variantResult = checkVariantWin(); if (variantResult) { setTimeout(() => showGameOver(variantResult), 400); return; }
if (!localMultiplayer && chess.turn() !== userColor) { botThinking = true; setTimeout(requestBotMove, 150); }
}
function requestBotMove() { if (state !== 'PLAYING' || eveMode) return; getBotMove(uci => { botThinking = false; if (!uci || state !== 'PLAYING') return; if (currentVariant === 'atomic') executeMoveAtomic(uci.slice(0,2), uci.slice(2,4), uci[4] || null); else if (currentVariant === 'horde') executeHordeMove(uci.slice(0,2), uci.slice(2,4), uci[4] || null); else executeMove(uci.slice(0,2), uci.slice(2,4), uci[4] || null); drawBoard(); updateSidebar(); updateVariantUI(); }); }
boardCvs.addEventListener('click', e => {
if (state !== 'PLAYING') return; if (document.getElementById('gameover-overlay').style.display !== 'none') return; if (botThinking) return; if (eveMode) return;
const rect = boardCvs.getBoundingClientRect(); const col = Math.floor((e.clientX - rect.left) / SQ); const row = Math.floor((e.clientY - rect.top) / SQ);
const persp = localMultiplayer ? chess.turn() : userColor; const sq = colRowToSq(col, row, persp); const activeColor = localMultiplayer ? chess.turn() : userColor;
if (selectedSq === null) {
if (currentVariant === 'crazyhouse' && czhSelectedPocket) {
if (executeCzhDrop(czhSelectedPocket.type, sq)) { drawBoard(); updateSidebar(); updateVariantUI(); if (!localMultiplayer && chess.turn() !== userColor) { botThinking = true; setTimeout(requestBotMove, 150); } return; }
czhSelectedPocket = null;
}
const p = chess.get(sq); if (p && p.color === activeColor) { selectedSq = sq; legalDots = currentVariant === 'horde' ? hMoves(chess.fen(), sq) : chess.moves({ square: sq, verbose: true }).map(m => m.to); }
}
else { if (currentVariant === 'horde') { const hLegal = hMoves(chess.fen(), selectedSq); if (hLegal.includes(sq)) { const _hp = hFen2Bd(chess.fen())[selectedSq]; const _isP = _hp && _hp.type === 'p' && ((_hp.color === 'w' && sq[1] === '8') || (_hp.color === 'b' && sq[1] === '1')); if (_isP) { pendingPromotion = { from: selectedSq, to: sq }; selectedSq = null; legalDots = []; drawBoard(); showPromotion(); return; } executeHordeMove(selectedSq, sq, null); selectedSq = null; legalDots = []; } else { const _p2 = hFen2Bd(chess.fen())[sq]; if (_p2 && _p2.color === activeColor) { selectedSq = sq; legalDots = hMoves(chess.fen(), sq); } else { selectedSq = null; legalDots = []; } } } else { const mvs = chess.moves({ square: selectedSq, verbose: true }); const match = mvs.find(m => m.to === sq);
if (match) { if (match.flags.includes('p')) { pendingPromotion = { from: selectedSq, to: sq }; selectedSq = null; legalDots = []; drawBoard(); showPromotion(); return; }
if (currentVariant === 'atomic') { if (executeMoveAtomic(selectedSq, sq, null)) { selectedSq = null; legalDots = []; const vr = checkVariantWin(); if (vr) { setTimeout(() => showGameOver(vr), 400); return; } if (!localMultiplayer && chess.turn() !== userColor) { botThinking = true; setTimeout(requestBotMove, 150); } } }
else { executeMove(selectedSq, sq, null); } selectedSq = null; legalDots = []; }
else { const p2 = chess.get(sq); if (p2 && p2.color === activeColor) { selectedSq = sq; legalDots = chess.moves({ square: sq, verbose: true }).map(m => m.to); } else { selectedSq = null; legalDots = []; } }
} } drawBoard(); updateSidebar(); updateVariantUI();
});
boardCvs.addEventListener('dragover', e => {
if (currentVariant !== 'crazyhouse' || state !== 'PLAYING' || eveMode || botThinking) return;
e.preventDefault();
});
boardCvs.addEventListener('drop', e => {
if (currentVariant !== 'crazyhouse' || state !== 'PLAYING' || eveMode || botThinking) return;
e.preventDefault();
const pieceType = e.dataTransfer.getData('czh-piece') || czhDragPiece;
czhDragPiece = null;
if (!pieceType) return;
const activeColor = localMultiplayer ? chess.turn() : userColor;
if (chess.turn() !== activeColor) return;
const rect = boardCvs.getBoundingClientRect();
const col = Math.floor((e.clientX - rect.left) / SQ);
const row = Math.floor((e.clientY - rect.top) / SQ);
const persp = localMultiplayer ? chess.turn() : userColor;
const sq = colRowToSq(col, row, persp);
czhSelectedPocket = { color: activeColor, type: pieceType };
if (executeCzhDrop(pieceType, sq)) {
drawBoard(); updateSidebar(); updateVariantUI();
if (!localMultiplayer && chess.turn() !== userColor) { botThinking = true; setTimeout(requestBotMove, 150); }
} else { czhSelectedPocket = null; }
});
function showPromotion() { const row = document.getElementById('promo-row'); row.innerHTML = ''; const pieces = chess.turn() === 'w' ? ['Q','R','B','N'] : ['q','r','b','n']; for (const p of pieces) { const b = document.createElement('button'); b.className = 'promo-btn'; b.textContent = SYMBOLS[p]; b.onclick = () => { document.getElementById('promo-overlay').style.display = 'none'; if (pendingPromotion) { if (currentVariant === 'atomic') executeMoveAtomic(pendingPromotion.from, pendingPromotion.to, p.toLowerCase()); else executeMove(pendingPromotion.from, pendingPromotion.to, p.toLowerCase()); pendingPromotion = null; drawBoard(); } }; row.appendChild(b); } document.getElementById('promo-overlay').style.display = 'flex'; }
function handlePocketClick(e) {
if (state !== 'PLAYING' || currentVariant !== 'crazyhouse') return;
if (eveMode || botThinking) return;
const myColor = localMultiplayer ? chess.turn() : userColor;
const pocket = czhPocket[myColor] || [];
if (!pocket.length) return;
// Show pocket selection — cycle through available pieces
if (czhSelectedPocket) { czhSelectedPocket = null; }
else if (pocket.length > 0) { czhSelectedPocket = { color: myColor, type: pocket[0] }; showToast(`Drop: ${SYMBOLS[myColor === 'w' ? pocket[0].toUpperCase() : pocket[0]] || pocket[0]} — click a square`); }
drawBoard(); updateSidebar();
}
document.getElementById('btm-captures').addEventListener('click', handlePocketClick);
function useHint() { if (hintsUsed >= 3 || !sf || !sfReady) return; if (chess.turn() !== userColor) return; hintsUsed++; sf.postMessage(`position fen ${chess.fen()}`); sf.postMessage('go depth 12 movetime 500'); pendingHintCb = uci => { if (uci && uci.length >= 4) { hintMove = { from: uci.slice(0,2), to: uci.slice(2,4) }; drawBoard(); } }; updateSidebar(); }
function doTakeback() { const limit = getTakebackLimit(); if (takebacksUsed >= limit || boardHistory.length === 0) return; botThinking = false; pendingBotCb = null; const steps = Math.min(2, boardHistory.length); for (let i = 0; i < steps; i++) boardHistory.pop(); if (boardHistory.length > 0) { const s = boardHistory[boardHistory.length - 1]; chess = new Chess(s.fen); capturedByWhite = [...s.capW]; capturedByBlack = [...s.capB]; moveHistory = [...s.hist]; } else { const _vfen2={'racingkings':'8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1','horde':'rnbqkbnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPP w kq - 0 1'}; chess = _vfen2[currentVariant] ? new Chess(_vfen2[currentVariant]) : new Chess(); capturedByWhite = []; capturedByBlack = []; moveHistory = []; } selectedSq = null; legalDots = []; hintMove = null; takebacksUsed++; updateSidebar(); drawBoard(); }
function openSettings() {
buildSettingsRows();
document.getElementById('settings-overlay').style.display = 'flex';
}
function closeSettings() {
document.getElementById('settings-overlay').style.display = 'none';
}
function buildSettingsRows() {
const container = document.getElementById('settings-rows');
container.innerHTML = '';
const rows = [
{ label: 'Move Sounds', key: 'moveSounds' },
{ label: 'Capture Sounds', key: 'captureSounds' },
{ label: 'Legal Dots', key: 'showDots' },
{ label: 'Check Sound', key: 'checkSounds' },
];
rows.forEach(({ label, key }) => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:16px';
const lbl = document.createElement('span');
lbl.textContent = label;
lbl.style.cssText = 'font-size:16px;font-weight:bold;color:#ffffff';
const tog = document.createElement('button');
tog.className = 'btn ' + (settings[key] ? 'win' : 'loss');
tog.textContent = settings[key] ? 'ON' : 'OFF';
tog.style.cssText = 'width:70px;padding:8px 0;font-size:14px';
tog.onclick = () => {
settings[key] = !settings[key];
saveSettings();
buildSettingsRows();
drawBoard();
};
row.appendChild(lbl);
row.appendChild(tog);
container.appendChild(row);
});
}
function showGameOver(msg) { clearInterval(timerInterval); document.getElementById('gameover-msg').textContent = msg; document.getElementById('gameover-overlay').style.display = 'flex'; }
function downloadPGN() { const w = userColor === 'w' ? 'Player' : 'Stockfish'; const b = userColor === 'b' ? 'Player' : 'Stockfish'; const pairs = []; for (let i = 0; i < moveHistory.length; i += 2) pairs.push(`${Math.floor(i/2)+1}. ${moveHistory[i]}${moveHistory[i+1] ? ' ' + moveHistory[i+1] : ''}`); const pgn = `[Event "OmniChess Game"]\n[Date "${new Date().toISOString().slice(0,10)}"]\n[White "${w}"]\n[Black "${b}"]\n\n${pairs.join(' ')}`; const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([pgn], { type: 'text/plain' })); a.download = `game_${Date.now()}.pgn`; a.click(); showToast('PGN saved!'); }
// ==================== PUZZLE ====================
async function fetchNewPuzzle() { puzzleLoading = true; puzzleChess = null; puzzleSolution = []; puzzleMoveIdx = 0; puzzleStatus = ""; puzzleSelectedSq = null; puzzleLegalDots = []; puzzleRating = ""; puzzleThemesStr = ""; puzzleStartTime = null; puzzleLastFeedback = ""; clearInterval(puzzleTimerInterval); puzzleDifficulty = PUZZLE_DIFFICULTIES[puzzleAdaptiveIdx]; document.getElementById('puz-turn').textContent = 'Loading puzzle...'; document.getElementById('puzzle-status-banner').style.display = 'none'; document.getElementById('puzzle-feedback').textContent = ''; try { const res = await fetch(`https://lichess.org/api/puzzle/next?difficulty=${puzzleDifficulty}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const pz = data.puzzle; puzzleSolution = pz.solution; puzzleRating = String(pz.rating || '?'); puzzleThemesStr = (pz.themes || []).slice(0, 3).join(', '); const tc = new Chess(); const sans = data.game.pgn.trim().split(/\s+/).filter(t => !/^\d+\./.test(t) && t.trim()); for (const mv of sans) { try { tc.move(mv); } catch(e) { break; } } const fm = puzzleSolution[0]; tc.move({ from: fm.slice(0,2), to: fm.slice(2,4), promotion: fm[4] || 'q' }); puzzleChess = new Chess(tc.fen()); puzzleMoveIdx = 1; puzzlePlayerColor = puzzleChess.turn(); puzzleLoading = false; puzzleStartTime = Date.now(); buildCoords(true); clearInterval(puzzleTimerInterval); puzzleTimerInterval = setInterval(() => { if (puzzleStatus === '' && puzzleStartTime) { const s = Math.floor((Date.now() - puzzleStartTime) / 1000); document.getElementById('puz-timer').textContent = `Time: ${s}s`; document.getElementById('puz-timer').style.color = '#f1c40f'; } }, 1000); } catch(e) { puzzleLoadError = `Could not load puzzle: ${e.message}`; puzzleLoading = false; document.getElementById('puz-turn').textContent = puzzleLoadError; return; } updatePuzzleUI(); drawPuzzleBoard(); }
function adaptPuzzleDifficulty(solved, elapsed) { if (!solved) { puzzleAdaptiveIdx = Math.max(0, puzzleAdaptiveIdx - 1); puzzleLastFeedback = `Too hard — dropping to ${PUZZLE_DIFFICULTIES[puzzleAdaptiveIdx].toUpperCase()}`; } else if (elapsed < PUZZLE_FAST_THRESHOLD) { puzzleAdaptiveIdx = Math.min(4, puzzleAdaptiveIdx + 1); puzzleLastFeedback = `Solved in ${Math.floor(elapsed)}s — bumping to ${PUZZLE_DIFFICULTIES[puzzleAdaptiveIdx].toUpperCase()}`; } else if (elapsed > PUZZLE_SLOW_THRESHOLD) { puzzleAdaptiveIdx = Math.max(0, puzzleAdaptiveIdx - 1); puzzleLastFeedback = `Solved in ${Math.floor(elapsed)}s — dropping to ${PUZZLE_DIFFICULTIES[puzzleAdaptiveIdx].toUpperCase()}`; } else { puzzleLastFeedback = `Solved in ${Math.floor(elapsed)}s — staying at ${PUZZLE_DIFFICULTIES[puzzleAdaptiveIdx].toUpperCase()}`; } }
function updatePuzzleUI() { if (!puzzleChess) return; document.getElementById('puz-turn').textContent = puzzleChess.turn() === 'w' ? 'WHITE to move' : 'BLACK to move'; const total = puzzleSolution.length - 1; const done = Math.max(0, puzzleMoveIdx - 1); document.getElementById('puz-progress').textContent = `Progress: ${done}/${total}`; document.getElementById('puz-diff').textContent = `Difficulty: ${puzzleDifficulty.toUpperCase()}`; document.getElementById('puz-rating').textContent = `Rating: ${puzzleRating} Themes: ${puzzleThemesStr}`; document.getElementById('puzzle-feedback').textContent = puzzleLastFeedback; const banner = document.getElementById('puzzle-status-banner'); if (puzzleStatus === 'solved') { banner.style.display = 'block'; banner.className = 'banner-solved'; banner.textContent = 'PUZZLE SOLVED!'; document.getElementById('puz-timer').style.color = '#888888'; } else if (puzzleStatus === 'failed') { banner.style.display = 'block'; banner.className = 'banner-failed'; banner.textContent = 'WRONG — Try Again'; document.getElementById('puz-timer').style.color = '#888888'; } else { banner.style.display = 'none'; } }
puzCvs.addEventListener('click', e => { if (state !== 'PUZZLE' || !puzzleChess || puzzleStatus === 'solved') return; if (puzzleChess.turn() !== puzzlePlayerColor) return; const rect = puzCvs.getBoundingClientRect(); const col = Math.floor((e.clientX - rect.left) / SQ); const row = Math.floor((e.clientY - rect.top) / SQ); const sq = colRowToSq(col, row, puzzlePlayerColor); if (puzzleSelectedSq === null) { const p = puzzleChess.get(sq); if (p && p.color === puzzlePlayerColor) { puzzleSelectedSq = sq; puzzleLegalDots = puzzleChess.moves({ square: sq, verbose: true }).map(m => m.to); } } else { if (puzzleMoveIdx < puzzleSolution.length) { const exp = puzzleSolution[puzzleMoveIdx]; const eFrom = exp.slice(0,2), eTo = exp.slice(2,4); if (puzzleSelectedSq === eFrom && sq === eTo) { const isCap = !!puzzleChess.get(eTo); playMoveSound(isCap); puzzleChess.move({ from: eFrom, to: eTo, promotion: exp[4] || 'q' }); puzzleMoveIdx++; puzzleStatus = ""; if (puzzleMoveIdx < puzzleSolution.length) { const opp = puzzleSolution[puzzleMoveIdx]; puzzleChess.move({ from: opp.slice(0,2), to: opp.slice(2,4), promotion: opp[4] || 'q' }); puzzleMoveIdx++; } if (puzzleMoveIdx >= puzzleSolution.length) { puzzleStatus = 'solved'; const elapsed = puzzleStartTime ? (Date.now() - puzzleStartTime) / 1000 : 999; adaptPuzzleDifficulty(true, elapsed); } } else if (puzzleLegalDots.includes(sq)) { puzzleStatus = 'failed'; const elapsed = puzzleStartTime ? (Date.now() - puzzleStartTime) / 1000 : 999; adaptPuzzleDifficulty(false, elapsed); } } puzzleSelectedSq = null; puzzleLegalDots = []; } updatePuzzleUI(); drawPuzzleBoard(); });
function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2500); }
buildMenu(); initStockfish();
</script>
</body>
</html>