bitstackchess 0.1.1

A bitboard‐based chess game engine with 10 × u128 move history
Documentation
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
#![allow(dead_code)]
#![allow(unused_variables)]

/// A 10-bit move code:
///   - high 4 bits = piece_index_within_side (0..15),
///   - low  6 bits = destination square (0..63).
///
/// We do NOT store a color bit here.  On ply k, “side to move” = k % 2:
///   - if k % 2 == 0 → White to move; global piece ID = piece_index_within_side (0..15)
///   - if k % 2 == 1 → Black to move; global piece ID = 16 + piece_index_within_side (16..31)
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Move10(u16);

impl Move10 {
    /// Construct Move10 from (piece_index_within_side, dest square).
    ///   - piece_id_within ∈ [0..16)
    ///   - dest ∈ [0..64)
    pub fn new(piece_id_within: u8, dest: u8) -> Move10 {
        assert!(piece_id_within < 16, "piece_id must fit in 4 bits");
        assert!(dest < 64,           "dest must fit in 6 bits");
        let bits: u16 = ((piece_id_within as u16) << 6) | (dest as u16);
        Move10(bits & 0x03FF) // mask to lower 10 bits
    }

    /// Extract the 4-bit “piece_index_within_side” (0..15).
    pub fn piece_id(&self) -> u8 {
        ((self.0 >> 6) & 0x0F) as u8
    }

    /// Extract the 6-bit “destination square” (0..63).
    pub fn dest(&self) -> u8 {
        (self.0 & 0x3F) as u8
    }
}

/// Holds exactly ten 128-bit “bit-planes.” Each `planes[i]` is a u128 whose bit k =
/// bit i of the 10-bit Move10 at ply k (for k = 0..127). This stores up to 128 plies of 10 bits each.
#[derive(Clone, Debug)]
pub struct MovePlanes {
    pub planes: [u128; 10],
}

impl MovePlanes {
    /// Create ten zeroed 128-bit words (no plies recorded).
    pub fn new() -> MovePlanes {
        MovePlanes { planes: [0u128; 10] }
    }

    /// Clear all ten planes (e.g. before replay).
    pub fn clear_all(&mut self) {
        for i in 0..10 {
            self.planes[i] = 0;
        }
    }

    /// Write (or erase) the 10-bit code for ply k.
    ///
    /// If `mv` is Some(Move10), for each bit index i = 0..9 we set `planes[i].bit(k) = (mv.0 >> i) & 1`.
    /// If `mv` is None, we clear bit k in all ten planes.
    ///
    /// # Panics
    /// Panics if `k >= 128`.
    pub fn write_ply(&mut self, k: usize, mv: Option<Move10>) {
        debug_assert!(k < 128, "ply index must be < 128");
        if let Some(mv10) = mv {
            let raw = mv10.0 as u32; // 10 bits in lower half
            for bit_i in 0..10 {
                let bit_val = ((raw >> bit_i) & 0x01) as u128;
                if bit_val == 1 {
                    self.planes[bit_i] |= 1u128 << k;
                } else {
                    self.planes[bit_i] &= !(1u128 << k);
                }
            }
        } else {
            // Clear bit k in all 10 planes
            for i in 0..10 {
                self.planes[i] &= !(1u128 << k);
            }
        }
    }

    /// Read back the 10-bit code stored at ply k and return it as Move10.
    ///
    /// # Panics
    /// Panics if `k >= 128`.
    pub fn read_ply(&self, k: usize) -> Move10 {
        debug_assert!(k < 128, "ply index must be < 128");
        let mut raw: u16 = 0;
        for bit_i in 0..10 {
            let bit_val = ((self.planes[bit_i] >> k) & 0x01) as u16;
            raw |= bit_val << bit_i;
        }
        Move10(raw)
    }
}

/// A 32-bit mask: bit i = 1 if global piece ID i is currently captured (off-board).
pub type CapturedBits = u32;

/// A 64-bit mask: bit s = 1 if square s (0..63) is occupied.
pub type Occupied = u64;

/// PieceMapping holds two parallel arrays for O(1) lookups:
///  • `piece_square[pid] = Option<square>`: where global piece pid ∈ [0..31] sits (or None if captured).
///  • `square_piece[sq]    = Option<pid>` : which global piece pid sits on square sq ∈ [0..63] (or None if empty).
#[derive(Clone, Debug)]
pub struct PieceMapping {
    pub piece_square: [Option<u8>; 32],
    pub square_piece: [Option<u8>; 64],
}

impl PieceMapping {
    /// Create an “all pieces off-board” mapping (all squares empty).
    pub fn new_empty() -> PieceMapping {
        PieceMapping {
            piece_square: [None; 32],
            square_piece: [None; 64],
        }
    }

    /// Place piece `pid` (0..31) onto square `sq` (0..63).
    /// Panics if that piece is already on-board, or that square is occupied.
    pub fn place_piece(&mut self, pid: u8, sq: u8) {
        let p = pid as usize;
        let s = sq as usize;
        assert!(
            self.piece_square[p].is_none(),
            "Piece {} was already on the board!", pid
        );
        assert!(
            self.square_piece[s].is_none(),
            "Square {} was already occupied!", sq
        );
        self.piece_square[p] = Some(sq);
        self.square_piece[s] = Some(pid);
    }

    /// Remove piece `pid` from the board (set it off-board).
    /// If it’s already off-board, do nothing.
    pub fn remove_piece(&mut self, pid: u8) {
        let p = pid as usize;
        if let Some(old_sq) = self.piece_square[p] {
            let s = old_sq as usize;
            self.square_piece[s] = None;
        }
        self.piece_square[p] = None;
    }

    /// Move piece `pid` from its current square to `dest`.
    /// Panics if the piece is not on the board or `dest` is occupied.
    pub fn move_piece(&mut self, pid: u8, dest: u8) {
        let p = pid as usize;
        let d = dest as usize;
        let old_sq = self.piece_square[p]
            .expect(&format!("Piece {} is not on board!", pid)) as usize;

        // Vacate old square:
        self.square_piece[old_sq] = None;

        // Occupy dest:
        assert!(
            self.square_piece[d].is_none(),
            "Destination square {} is occupied!", dest
        );
        self.square_piece[d] = Some(pid);
        self.piece_square[p] = Some(dest);
    }

    /// Return `Some(pid)` if square `sq` is occupied, else `None`.
    pub fn who_on_square(&self, sq: u8) -> Option<u8> {
        self.square_piece[sq as usize]
    }
}

/// The full “game state,” including en passant tracking and a corrected castling implementation.
#[derive(Clone, Debug)]
pub struct ChessGame {
    /// Ten 128-bit planes storing up to 128 plies of 10 bits each.
    pub planes:         MovePlanes,

    /// How many half-moves (plies) have been recorded. Next free index = `ply`.
    pub ply:            usize,  // 0..=128

    /// A 32-bit mask: bit i = 1 if global piece i is captured/off-board.
    pub captured_bits:  CapturedBits,

    /// A 64-bit mask: bit s = 1 if square s (0..63) is occupied.
    pub occupied:       Occupied,

    /// O(1) lookups from pid ↔ square.
    pub mapping:        PieceMapping,

    /// If the last move was a pawn double-step, this is the square “behind” it that can be captured
    /// by en passant on the very next ply. Otherwise, None.
    pub en_passant_target: Option<u8>,
}

impl ChessGame {
    /// Create a new game in the standard starting position.
    pub fn new() -> ChessGame {
        let mut g = ChessGame {
            planes: MovePlanes::new(),
            ply: 0,
            captured_bits: 0,
            occupied: 0,
            mapping: PieceMapping::new_empty(),
            en_passant_target: None,
        };
        g.set_starting_position();
        g
    }

    /// Reset board-state to the standard starting position (does NOT clear `planes` or `ply`).
    pub fn set_starting_position(&mut self) {
        self.captured_bits = 0;
        self.occupied = 0;
        self.mapping = PieceMapping::new_empty();
        self.en_passant_target = None;

        // White pawns: global IDs 0..7 on rank 2 (squares 8..15)
        for i in 0..8 {
            let pid = i as u8;       // 0..7
            let sq  = (8 + i) as u8; // 8..15
            self.mapping.place_piece(pid, sq);
            self.occupied |= 1u64 << sq;
        }
        // White knights: global IDs 8, 9 on b1=1, g1=6
        self.mapping.place_piece( 8,  1);  self.occupied |= 1u64 <<  1;
        self.mapping.place_piece( 9,  6);  self.occupied |= 1u64 <<  6;
        // White bishops: global IDs 10, 11 on c1=2, f1=5
        self.mapping.place_piece(10,  2);  self.occupied |= 1u64 <<  2;
        self.mapping.place_piece(11,  5);  self.occupied |= 1u64 <<  5;
        // White rooks: global IDs 12, 13 on a1=0, h1=7
        self.mapping.place_piece(12,  0);  self.occupied |= 1u64 <<  0;
        self.mapping.place_piece(13,  7);  self.occupied |= 1u64 <<  7;
        // White queen: PID 14 on d1=3
        self.mapping.place_piece(14,  3);  self.occupied |= 1u64 <<  3;
        // White king:  PID 15 on e1=4
        self.mapping.place_piece(15,  4);  self.occupied |= 1u64 <<  4;

        // Black pawns: global IDs 16..23 on rank 7 (squares 48..55)
        for i in 0..8 {
            let pid = (16 + i) as u8; // 16..23
            let sq  = (48 + i) as u8; // 48..55
            self.mapping.place_piece(pid, sq);
            self.occupied |= 1u64 << sq;
        }
        // Black knights: IDs 24, 25 on b8=57, g8=62
        self.mapping.place_piece(24, 57);  self.occupied |= 1u64 << 57;
        self.mapping.place_piece(25, 62);  self.occupied |= 1u64 << 62;
        // Black bishops: IDs 26, 27 on c8=58, f8=61
        self.mapping.place_piece(26, 58);  self.occupied |= 1u64 << 58;
        self.mapping.place_piece(27, 61);  self.occupied |= 1u64 << 61;
        // Black rooks: IDs 28, 29 on a8=56, h8=63
        self.mapping.place_piece(28, 56);  self.occupied |= 1u64 << 56;
        self.mapping.place_piece(29, 63);  self.occupied |= 1u64 << 63;
        // Black queen: ID 30 on d8=59
        self.mapping.place_piece(30, 59);  self.occupied |= 1u64 << 59;
        // Black king:  ID 31 on e8=60
        self.mapping.place_piece(31, 60);  self.occupied |= 1u64 << 60;
    }

    /// Push one half-move (ply):
    ///
    ///   • `piece_id_within` ∈ [0..16) is the index of the piece _within_ the side to move.
    ///   • `dest` ∈ [0..64) is the destination square.
    ///
    /// We compute `color = self.ply % 2`: 0=White, 1=Black ⇒ `global_pid = 16*color + piece_id_within`.
    ///
    /// Steps:
    ///   1. Determine `color` and `global_pid`.  
    ///   2. Find `src` = `mapping.piece_square[global_pid]`. Panic if None.  
    ///   3a. **Castling** (king moves two files from start square) ⇒ directly relocate known rook‐PID.  
    ///   3b. **En passant capture** (if pawn moves diagonally onto `en_passant_target`, remove captured pawn).  
    ///   3c. **Normal capture** (if `dest` occupied, capture that occupant).  
    ///   4. Move `global_pid` from `src` → `dest` (update `mapping`/`occupied`).  
    ///   5. If pawn double-step, set `en_passant_target`; otherwise clear it.  
    ///   6. Record `(piece_id_within<<6)|dest` into `planes` at index = old `ply`.  
    ///   7. Increment `self.ply`.  
    ///
    /// # Panics
    /// - if `piece_id_within >= 16` or `dest >= 64` or `ply >= 128`.
    pub fn push_move(&mut self, piece_id_within: u8, dest: u8) {
        assert!(piece_id_within < 16, "piece_id must be 0..15");
        assert!(dest < 64, "dest must be 0..63");
        let k = self.ply;
        assert!(k < 128, "Cannot exceed 128 plies");

        // 1) Determine side and global PID
        let color = (k % 2) as u8;                   // 0=White, 1=Black
        let global_pid = 16 * color + piece_id_within; // White: 0..15; Black: 16..31

        // 2) Find source square of that piece
        let src = self.mapping.piece_square[global_pid as usize]
            .expect(&format!("Piece {} not on board!", global_pid)) as usize;

        // Precompute files and ranks
        let src_file = (src % 8) as i8;
        let src_rank = (src / 8) as i8;
        let dst_file = (dest % 8) as i8;
        let dst_rank = (dest / 8) as i8;

        // 3a) **CASTLING**: If king (pid_within == 15) moves two files from its start:
        if piece_id_within == 15 {
            let king_start = if color == 0 { 4 } else { 60 };
            if src == king_start as usize && (dst_file - src_file).abs() == 2 {
                if dst_file == src_file + 2 {
                    // Kingside castling:
                    //  White: rook‐PID = 13 (h1=7) → f1=5
                    //  Black: rook‐PID = 29 (h8=63) → f8=61
                    let rook_pid = if color == 0 { 13 } else { 29 };
                    let rook_src = if color == 0 { 7  } else { 63 };
                    let rook_dst = if color == 0 { 5  } else { 61 };

                    // --- Move king directly:
                    self.mapping.piece_square[global_pid as usize] = Some(dest);
                    self.mapping.square_piece[dest as usize] = Some(global_pid);
                    self.mapping.square_piece[src as usize] = None;
                    self.occupied &= !(1u64 << (src as u64));
                    self.occupied |=  1u64 << (dest as u64);

                    // --- Move rook directly:
                    self.mapping.piece_square[rook_pid as usize] = Some(rook_dst);
                    self.mapping.square_piece[rook_dst as usize] = Some(rook_pid);
                    self.mapping.square_piece[rook_src as usize] = None;
                    self.occupied &= !(1u64 << (rook_src as u64));
                    self.occupied |=  1u64 << (rook_dst as u64);

                    // Clear en passant
                    self.en_passant_target = None;

                    // Record Move10 for the king’s move
                    let mv10 = Move10::new(piece_id_within, dest);
                    self.planes.write_ply(k, Some(mv10));
                    self.ply += 1;
                    return;
                } else if dst_file == src_file - 2 {
                    // Queenside castling:
                    //  White: rook‐PID = 12 (a1=0) → d1=3
                    //  Black: rook‐PID = 28 (a8=56) → d8=59
                    let rook_pid = if color == 0 { 12 } else { 28 };
                    let rook_src = if color == 0 { 0  } else { 56 };
                    let rook_dst = if color == 0 { 3  } else { 59 };

                    // --- Move king directly:
                    self.mapping.piece_square[global_pid as usize] = Some(dest);
                    self.mapping.square_piece[dest as usize] = Some(global_pid);
                    self.mapping.square_piece[src as usize] = None;
                    self.occupied &= !(1u64 << (src as u64));
                    self.occupied |=  1u64 << (dest as u64);

                    // --- Move rook directly:
                    self.mapping.piece_square[rook_pid as usize] = Some(rook_dst);
                    self.mapping.square_piece[rook_dst as usize] = Some(rook_pid);
                    self.mapping.square_piece[rook_src as usize] = None;
                    self.occupied &= !(1u64 << (rook_src as u64));
                    self.occupied |=  1u64 << (rook_dst as u64);

                    // Clear en passant
                    self.en_passant_target = None;

                    // Record Move10 for the king’s move
                    let mv10 = Move10::new(piece_id_within, dest);
                    self.planes.write_ply(k, Some(mv10));
                    self.ply += 1;
                    return;
                }
            }
        }

        // 3b) **EN PASSANT CAPTURE:** Pawn (pid_within < 8) moves diagonally to an empty `dest`:
        let is_pawn = piece_id_within < 8;
        if is_pawn && (dst_file - src_file).abs() == 1 && (dst_rank - src_rank).abs() == 1 {
            if self.mapping.who_on_square(dest).is_none() {
                if let Some(ep_sq) = self.en_passant_target {
                    if ep_sq == dest {
                        // Remove pawn “behind” dest:
                        let captured_sq = if color == 0 {
                            (dest as i8 - 8) as u8
                        } else {
                            (dest as i8 + 8) as u8
                        };
                        if let Some(opp_pid) = self.mapping.who_on_square(captured_sq) {
                            self.captured_bits |= 1u32 << (opp_pid as u32);
                            self.mapping.remove_piece(opp_pid);
                            self.occupied &= !(1u64 << (captured_sq as u64));
                        }
                    }
                }
            }
        }

        // 3c) **Normal capture:** if `dest` was occupied
        if let Some(opp_pid) = self.mapping.who_on_square(dest) {
            self.captured_bits |= 1u32 << (opp_pid as u32);
            self.mapping.remove_piece(opp_pid);
            self.occupied &= !(1u64 << (dest as u64));
        }

        // 4) **Move** `global_pid` from `src` → `dest`
        self.mapping.move_piece(global_pid as u8, dest);
        self.occupied &= !(1u64 << (src as u64));
        self.occupied |=  1u64 << (dest as u64);

        // 5) **Set/clear `en_passant_target`** if pawn double-step
        if is_pawn && (dst_rank - src_rank).abs() == 2 {
            let ep_square = if color == 0 {
                (dest as i8 - 8) as u8
            } else {
                (dest as i8 + 8) as u8
            };
            self.en_passant_target = Some(ep_square);
        } else {
            self.en_passant_target = None;
        }

        // 6) Pack and record the 10 bits
        let mv10 = Move10::new(piece_id_within, dest);
        self.planes.write_ply(k, Some(mv10));

        // 7) Increment `ply`
        self.ply += 1;
    }

    /// Pop (undo) the last half-move, if any, then replay plies 0..(ply−1) to rebuild state.
    pub fn pop_move(&mut self) {
        if self.ply == 0 {
            return;
        }
        // 1) Decrement `ply`
        self.ply -= 1;
        let new_k = self.ply;

        // 2) Erase bit `new_k` in all ten planes
        self.planes.write_ply(new_k, None);

        // 3) Reset board & clear en passant
        self.captured_bits = 0;
        self.occupied = 0;
        self.mapping = PieceMapping::new_empty();
        self.en_passant_target = None;
        self.set_starting_position();

        // 4) Replay plies 0..(new_k−1)
        for i in 0..new_k {
            let mv10 = self.planes.read_ply(i);
            let pid_within = mv10.piece_id(); // 0..15
            let dst = mv10.dest();            // 0..63

            let color = (i % 2) as u8;               // 0=White, 1=Black
            let global_pid = 16 * color + pid_within; // 0..31

            let src = self.mapping.piece_square[global_pid as usize]
                .expect(&format!("(Replay) piece {} missing at ply {}", global_pid, i)) as usize;

            let src_file = (src % 8) as i8;
            let src_rank = (src / 8) as i8;
            let dst_file = (dst % 8) as i8;
            let dst_rank = (dst / 8) as i8;

            // 4a) **CASTLING?** if pid_within == 15 and king moves two files
            if pid_within == 15 {
                let king_start = if color == 0 { 4 } else { 60 };
                if src == king_start as usize && (dst_file - src_file).abs() == 2 {
                    if dst_file == src_file + 2 {
                        // Kingside
                        let rook_pid = if color == 0 { 13 } else { 29 };
                        let rook_src = if color == 0 { 7  } else { 63 };
                        let rook_dst = if color == 0 { 5  } else { 61 };

                        // Move king:
                        self.mapping.piece_square[global_pid as usize] = Some(dst);
                        self.mapping.square_piece[dst as usize] = Some(global_pid);
                        self.mapping.square_piece[src as usize] = None;
                        self.occupied &= !(1u64 << (src as u64));
                        self.occupied |=  1u64 << (dst as u64);

                        // Move rook:
                        self.mapping.piece_square[rook_pid as usize] = Some(rook_dst);
                        self.mapping.square_piece[rook_dst as usize] = Some(rook_pid);
                        self.mapping.square_piece[rook_src as usize] = None;
                        self.occupied &= !(1u64 << (rook_src as u64));
                        self.occupied |=  1u64 << (rook_dst as u64);

                        self.en_passant_target = None;
                        continue;
                    } else if dst_file == src_file - 2 {
                        // Queenside
                        let rook_pid = if color == 0 { 12 } else { 28 };
                        let rook_src = if color == 0 { 0  } else { 56 };
                        let rook_dst = if color == 0 { 3  } else { 59 };

                        // Move king:
                        self.mapping.piece_square[global_pid as usize] = Some(dst);
                        self.mapping.square_piece[dst as usize] = Some(global_pid);
                        self.mapping.square_piece[src as usize] = None;
                        self.occupied &= !(1u64 << (src as u64));
                        self.occupied |=  1u64 << (dst as u64);

                        // Move rook:
                        self.mapping.piece_square[rook_pid as usize] = Some(rook_dst);
                        self.mapping.square_piece[rook_dst as usize] = Some(rook_pid);
                        self.mapping.square_piece[rook_src as usize] = None;
                        self.occupied &= !(1u64 << (rook_src as u64));
                        self.occupied |=  1u64 << (rook_dst as u64);

                        self.en_passant_target = None;
                        continue;
                    }
                }
            }

            // 4b) **EN PASSANT?**
            let is_pawn = pid_within < 8;
            if is_pawn && (dst_file - src_file).abs() == 1 && (dst_rank - src_rank).abs() == 1 {
                if self.mapping.who_on_square(dst).is_none() {
                    if let Some(ep_sq) = self.en_passant_target {
                        if ep_sq == dst {
                            let captured_sq = if color == 0 {
                                (dst as i8 - 8) as u8
                            } else {
                                (dst as i8 + 8) as u8
                            };
                            if let Some(opp_pid) = self.mapping.who_on_square(captured_sq) {
                                self.captured_bits |= 1u32 << (opp_pid as u32);
                                self.mapping.remove_piece(opp_pid);
                                self.occupied &= !(1u64 << (captured_sq as u64));
                            }
                        }
                    }
                }
            }

            // 4c) **Normal capture**?
            if let Some(opp_pid) = self.mapping.who_on_square(dst) {
                self.captured_bits |= 1u32 << (opp_pid as u32);
                self.mapping.remove_piece(opp_pid);
                self.occupied &= !(1u64 << (dst as u64));
            }

            // 4d) **Move the piece**
            self.mapping.move_piece(global_pid as u8, dst);
            self.occupied &= !(1u64 << (src as u64));
            self.occupied |=  1u64 << (dst as u64);

            // 4e) **Set/clear `en_passant_target`**
            if is_pawn && (dst_rank - src_rank).abs() == 2 {
                let ep_square = if color == 0 {
                    (dst as i8 - 8) as u8
                } else {
                    (dst as i8 + 8) as u8
                };
                self.en_passant_target = Some(ep_square);
            } else {
                self.en_passant_target = None;
            }
        }
    }
}

/// Encode a square (rank, file) as a 6-bit number: (rank << 3) | file.
/// Both `rank` and `file` must be in 0..=7.
pub fn encode_square(rank: u8, file: u8) -> u8 {
    assert!(rank < 8 && file < 8, "rank/file must be 0..7");
    (rank << 3) | file
}

/// Encode a piece ID as 5 bits according to:
///  • Bit 4 (MSB): color (0=White, 1=Black)
///  • Bit 3: is_pawn (1=pawn, 0=other)
///  • Bits 2..0:
///      – if is_pawn==1: bits 2..0 = pawn spawn rank (0..7)
///      – if is_pawn==0: bit 2 = spawn_side (0=east, 1=west),
///                      bits 1..0 = kind (00=royal, 01=bishop, 10=knight, 11=rook)
///
/// Examples:
///  • White King   = 0_0_0_000 = 0x00
///  • White Queen  = 0_0_1_000 = 0x04
///  • Black King   = 1_0_0_000 = 0x10
///  • Black Queen  = 1_0_1_000 = 0x14
///  • White Pawn   = 0_1_(rank) = (e.g. rank=2 => 0_1_010 = 0x0A)
///  • Black Pawn   = 1_1_(rank) = (e.g. rank=6 => 1_1_110 = 0x1E)
pub fn encode_piece(color: u8, is_pawn: bool, spawn_side_or_rank: u8, kind_or_unused: u8) -> u8 {
    // color: 0 or 1
    // if pawn: spawn_side_or_rank = rank (0..7); ignore kind_or_unused
    // if not pawn: spawn_side_or_rank = spawn_side (0 or 1), kind_or_unused = kind (0..3)
    let bit_color = (color & 1) << 4;
    let bit_pawn = (is_pawn as u8) << 3;
    let lower3 = if is_pawn {
        spawn_side_or_rank & 0b111
    } else {
        let side_bit = (spawn_side_or_rank & 1) << 2;
        let kind_bits = kind_or_unused & 0b11;
        side_bit | kind_bits
    };
    bit_color | bit_pawn | lower3
}

/// Return a Vec of (piece_id, square_id) tuples for the standard starting position.
/// We use zero-based ranks/files:
///  • White back rank = rank 0 (♖♘♗♕♔♗♘♖) on files 0..7  
///  • White pawns = rank 1 on files 0..7  
///  • Black pawns = rank 6 on files 0..7  
///  • Black back rank = rank 7 (♜♞♝♛♚♝♞♜) on files 0..7
///
/// Piece IDs (5 bits) follow the `encode_piece` scheme above.
pub fn init_chess_positions() -> Vec<(u8, u8)> {
    let mut v = Vec::new();

    // 1) White back rank (rank=0), from file=0..7: R, N, B, Q, K, B, N, R
    //   – West side = files 0,1,2,3  (queen-side)
    //   – East side = files 4,5,6,7  (king-side)
    // Kind mapping for non-pawns: 
    //   royal=0, bishop=1, knight=2, rook=3
    //  spawn_side: 0=east, 1=west
    //  color=0 (White), is_pawn=0
    //  So piece_id = (0<<4) | (0<<3) | (spawn_side<<2) | kind
    //
    //  file=0 (a1): white rook (west side, kind=3) => spawn_side=1, kind=3 => bits=0_0_1_11 = 0x07
    //  file=1 (b1): white knight (west, kind=2) => 0_0_1_10 = 0x06
    //  file=2 (c1): white bishop (west, kind=1) => 0_0_1_01 = 0x05
    //  file=3 (d1): white queen  (west, kind=0) => 0_0_1_00 = 0x04
    //  file=4 (e1): white king   (east, kind=0) => 0_0_0_00 = 0x00
    //  file=5 (f1): white bishop (east, kind=1) => 0_0_0_01 = 0x01
    //  file=6 (g1): white knight (east, kind=2) => 0_0_0_10 = 0x02
    //  file=7 (h1): white rook   (east, kind=3) => 0_0_0_11 = 0x03
    let white_back: [(u8, u8); 8] = [
        (encode_piece(0, false, 1, 3), encode_square(0, 0)), // ♖ a1
        (encode_piece(0, false, 1, 2), encode_square(0, 1)), // ♘ b1
        (encode_piece(0, false, 1, 1), encode_square(0, 2)), // ♗ c1
        (encode_piece(0, false, 1, 0), encode_square(0, 3)), // ♕ d1
        (encode_piece(0, false, 0, 0), encode_square(0, 4)), // ♔ e1
        (encode_piece(0, false, 0, 1), encode_square(0, 5)), // ♗ f1
        (encode_piece(0, false, 0, 2), encode_square(0, 6)), // ♘ g1
        (encode_piece(0, false, 0, 3), encode_square(0, 7)), // ♖ h1
    ];
    v.extend_from_slice(&white_back);

    // 2) White pawns (rank=1, files=0..7), color=0, is_pawn=1, spawn_rank=1:
    //    piece_id = 0_1_001 = 0b0_1_001 = 0x09
    //
    let white_pawn_id = encode_piece(0, true, 1, 0);
    for file in 0..8 {
        v.push((white_pawn_id, encode_square(1, file)));
    }

    // 3) Black pawns (rank=6, files=0..7), color=1, is_pawn=1, spawn_rank=6:
    //    piece_id = 1_1_110 = 0b1_1_110 = 0x1E
    let black_pawn_id = encode_piece(1, true, 6, 0);
    for file in 0..8 {
        v.push((black_pawn_id, encode_square(6, file)));
    }

    // 4) Black back rank (rank=7), from file=0..7: ♜,♞,♝,♛,♚,♝,♞,♜
    //    color=1, is_pawn=0
    //
    //  file=0 (a8): black rook   (west, kind=3) => 1_0_1_11 = 0b1_0_1_11 = 0x17
    //  file=1 (b8): black knight (west, kind=2) => 1_0_1_10 = 0x16
    //  file=2 (c8): black bishop (west, kind=1) => 1_0_1_01 = 0x15
    //  file=3 (d8): black queen  (west, kind=0) => 1_0_1_00 = 0x14
    //  file=4 (e8): black king   (east, kind=0) => 1_0_0_00 = 0x10
    //  file=5 (f8): black bishop (east, kind=1) => 1_0_0_01 = 0x11
    //  file=6 (g8): black knight (east, kind=2) => 1_0_0_10 = 0x12
    //  file=7 (h8): black rook   (east, kind=3) => 1_0_0_11 = 0x13
    let black_back: [(u8, u8); 8] = [
        (encode_piece(1, false, 1, 3), encode_square(7, 0)), // ♜ a8
        (encode_piece(1, false, 1, 2), encode_square(7, 1)), // ♞ b8
        (encode_piece(1, false, 1, 1), encode_square(7, 2)), // ♝ c8
        (encode_piece(1, false, 1, 0), encode_square(7, 3)), // ♛ d8
        (encode_piece(1, false, 0, 0), encode_square(7, 4)), // ♚ e8
        (encode_piece(1, false, 0, 1), encode_square(7, 5)), // ♝ f8
        (encode_piece(1, false, 0, 2), encode_square(7, 6)), // ♞ g8
        (encode_piece(1, false, 0, 3), encode_square(7, 7)), // ♜ h8
    ];
    v.extend_from_slice(&black_back);

    v
}



#[cfg(test)]
mod tests {
    use super::*;

    // ──────────────────────────────────────────────────────────────────────
    // 0) Basic helper tests (each only once)
    // ──────────────────────────────────────────────────────────────────────

    #[test]
    fn test_move10_roundtrip() {
        for pid in 0..16 {
            for dst in 0..64 {
                let mv = Move10::new(pid, dst);
                assert_eq!(mv.piece_id(), pid);
                assert_eq!(mv.dest(), dst);
            }
        }
    }

    #[test]
    fn test_move_planes_write_read() {
        let mut planes = MovePlanes::new();
        let codes = [
            Move10::new(3, 10),
            Move10::new(15, 63),
            Move10::new(0, 0),
            Move10::new(7, 31),
        ];
        for (i, &mv) in codes.iter().enumerate() {
            planes.write_ply(i, Some(mv));
        }
        for (i, &mv) in codes.iter().enumerate() {
            let got = planes.read_ply(i);
            assert_eq!(got, mv);
        }
        planes.write_ply(2, None);
        let cleared = planes.read_ply(2);
        assert_eq!(cleared.0, 0);
    }

    #[test]
    fn test_piece_mapping_basic() {
        let mut pm = PieceMapping::new_empty();
        pm.place_piece(5, 20);
        assert_eq!(pm.piece_square[5], Some(20));
        assert_eq!(pm.who_on_square(20), Some(5));
        pm.move_piece(5, 30);
        assert_eq!(pm.piece_square[5], Some(30));
        assert_eq!(pm.who_on_square(20), None);
        assert_eq!(pm.who_on_square(30), Some(5));
        pm.remove_piece(5);
        assert_eq!(pm.piece_square[5], None);
        assert_eq!(pm.who_on_square(30), None);
    }

    // ──────────────────────────────────────────────────────────────────────
    // 1) White Kingside Castling (fresh ChessGame)
    // ──────────────────────────────────────────────────────────────────────

    #[test]
    fn test_white_kingside_castling() {
        let mut game = ChessGame::new();

        // 1) White knight g1=6 → g3=22 (pid_within=9)
        game.push_move(9, 22);
        // 2) Black dummy: pawn a7=48 → a6=40 (pid_within=0)
        game.push_move(0, 40);
        // 3) White bishop f1=5 → e2=12 (pid_within=11)
        game.push_move(11, 12);
        // 4) Black dummy: pawn a6=40 → a5=32 (pid_within=0)
        game.push_move(0, 32);

        // Now path f1/g1 is clear, so White can castle:
        assert_eq!(game.mapping.piece_square[15], Some(4)); // king on e1=4
        assert_eq!(game.mapping.piece_square[13], Some(7)); // rook on h1=7
        game.push_move(15, 6); // castle: king e1=4 → g1=6

        // After castling: king at g1=6, rook at f1=5
        assert_eq!(game.mapping.piece_square[15], Some(6));
        assert_eq!(game.mapping.piece_square[13], Some(5));

        // Undo all 5 plies → back to starting position
        for _ in 0..5 {
            game.pop_move();
        }
        assert_eq!(game.mapping.piece_square[15], Some(4)); // back on e1=4
        assert_eq!(game.mapping.piece_square[13], Some(7)); // back on h1=7
        assert_eq!(game.ply, 0);
    }

    // ──────────────────────────────────────────────────────────────────────
    // 2) Black Kingside Castling (fresh ChessGame)
    // ──────────────────────────────────────────────────────────────────────

    #[test]
    fn test_black_kingside_castling() {
        let mut game = ChessGame::new();

        // 1) White dummy: pawn a2=8 → a3=16 (pid_within=0)
        game.push_move(0, 16);
        // 2) Black knight g8=62 → h6=55 (pid_within=9)
        game.push_move(9, 55);
        // 3) White dummy: pawn a3=16 → a4=24 (pid_within=0)
        game.push_move(0, 24);
        // 4) Black bishop f8=61 → g7=54 (pid_within=11)
        game.push_move(11, 54);

        // f8=61 and g8=62 are empty, but it's still White's turn at ply=4.
        // Insert one clean White move to hand the turn to Black at ply=5:
        // e.g. pawn b2=9 → b3=17 (pid_within=1)
        game.push_move(1, 17);

        // Now Black (ply=5) can castle:
        assert_eq!(game.mapping.piece_square[31], Some(60)); // Black king on e8=60
        assert_eq!(game.mapping.piece_square[29], Some(63)); // Black rook on h8=63
        game.push_move(15, 62); // castle: king e8=60 → g8=62

        // After castling: black king at g8=62, black rook at f8=61
        assert_eq!(game.mapping.piece_square[31], Some(62));
        assert_eq!(game.mapping.piece_square[29], Some(61));

        // Undo all 6 plies → back to starting position
        for _ in 0..6 {
            game.pop_move();
        }
        assert_eq!(game.mapping.piece_square[31], Some(60)); // back on e8=60
        assert_eq!(game.mapping.piece_square[29], Some(63)); // back on h8=63
        assert_eq!(game.ply, 0);
    }

    // ──────────────────────────────────────────────────────────────────────
    // 3) En Passant Capture (fresh ChessGame, legal 3‐ply EP)
    // ──────────────────────────────────────────────────────────────────────

    #[test]
    fn test_en_passant_capture() {
        let mut ep_game = ChessGame::new();

        // 1) White pawn e2=12 → e4=28 (pid_within=4 for White)
        ep_game.push_move(4, 28);
        // Now EP target should be e3=20
        assert_eq!(ep_game.en_passant_target, Some(20));

        // 2) Black dummy: pawn a7=48 → a6=40 (pid_within=0 for Black)
        ep_game.push_move(0, 40);

        // 3) White pawn e4=28 → e5=36 (pid_within=4)
        ep_game.push_move(4, 36);

        // 4) Black pawn d7=51 → d5=35 (pid_within=3)
        ep_game.push_move(3, 35);
        // Now EP target must be d6=43
        assert_eq!(ep_game.en_passant_target, Some(43));

        // 5) White pawn from e5=36 captures en passant by moving to d6=43
        ep_game.push_move(4, 43);
        // That should remove Black’s pawn on d5=35
        assert_eq!(ep_game.captured_bits & (1u32 << (16 + 3)), 1u32 << (16 + 3));
        // White pawn now sits on d6=43
        assert_eq!(ep_game.mapping.who_on_square(43), Some(4));

        // Undo all 5 plies → back to starting position
        for _ in 0..5 {
            ep_game.pop_move();
        }
        assert_eq!(ep_game.mapping.who_on_square(43), None);
        assert_eq!(ep_game.en_passant_target, None);
        assert_eq!(ep_game.ply, 0);
    }

    #[test]
    fn test_encode_square_basic() {
        // corners and center
        assert_eq!(encode_square(0, 0), 0b000_000); // a1
        assert_eq!(encode_square(0, 7), 0b000_111); // h1
        assert_eq!(encode_square(7, 0), 0b111_000); // a8
        assert_eq!(encode_square(7, 7), 0b111_111); // h8
        assert_eq!(encode_square(3, 4), (3 << 3) | 4); // d4 (rank=3,file=4)
    }

    #[test]
    fn test_encode_piece_non_pawns() {
        // White King (east, kind=royal=0): 0_0_0_000 = 0x00
        assert_eq!(encode_piece(0, false, 0, 0), 0x00);
        // White Queen (west, kind=royal=0): 0_0_1_000 = 0x04
        assert_eq!(encode_piece(0, false, 1, 0), 0x04);
        // White Bishop east (f1): 0_0_0_001 = 0x01
        assert_eq!(encode_piece(0, false, 0, 1), 0x01);
        // White Bishop west (c1): 0_0_1_001 = 0x05
        assert_eq!(encode_piece(0, false, 1, 1), 0x05);
        // White Knight east (g1): 0_0_0_010 = 0x02
        assert_eq!(encode_piece(0, false, 0, 2), 0x02);
        // White Knight west (b1): 0_0_1_010 = 0x06
        assert_eq!(encode_piece(0, false, 1, 2), 0x06);
        // White Rook east (h1): 0_0_0_011 = 0x03
        assert_eq!(encode_piece(0, false, 0, 3), 0x03);
        // White Rook west (a1): 0_0_1_011 = 0x07
        assert_eq!(encode_piece(0, false, 1, 3), 0x07);

        // Black King east (e8): 1_0_0_000 = 0x10
        assert_eq!(encode_piece(1, false, 0, 0), 0x10);
        // Black Queen west (d8): 1_0_1_000 = 0x14
        assert_eq!(encode_piece(1, false, 1, 0), 0x14);
        // Black Bishop east (f8): 1_0_0_001 = 0x11
        assert_eq!(encode_piece(1, false, 0, 1), 0x11);
        // Black Bishop west (c8): 1_0_1_001 = 0x15
        assert_eq!(encode_piece(1, false, 1, 1), 0x15);
        // Black Knight east (g8): 1_0_0_010 = 0x12
        assert_eq!(encode_piece(1, false, 0, 2), 0x12);
        // Black Knight west (b8): 1_0_1_010 = 0x16
        assert_eq!(encode_piece(1, false, 1, 2), 0x16);
        // Black Rook east (h8): 1_0_0_011 = 0x13
        assert_eq!(encode_piece(1, false, 0, 3), 0x13);
        // Black Rook west (a8): 1_0_1_011 = 0x17
        assert_eq!(encode_piece(1, false, 1, 3), 0x17);
    }

    #[test]
    fn test_encode_piece_pawns() {
        // White Pawn spawn rank = 1  => 0_1_001 = 0x09
        assert_eq!(encode_piece(0, true, 1, 0), 0x09);
        // All white pawns should share 0x09
        for _ in 0..8 {
            assert_eq!(encode_piece(0, true, 1, 0), 0x09);
        }

        // Black Pawn spawn rank = 6 => 1_1_110 = 0x1E
        assert_eq!(encode_piece(1, true, 6, 0), 0x1E);
        // All black pawns should share 0x1E
        for _ in 0..8 {
            assert_eq!(encode_piece(1, true, 6, 0), 0x1E);
        }
    }

    #[test]
    fn test_init_chess_positions() {
        let pos = init_chess_positions();

        // There should be exactly 32 entries
        assert_eq!(pos.len(), 32);

        // Collect them into a map (piece_id -> Vec<square_id>)
        let mut map: std::collections::BTreeMap<u8, Vec<u8>> = Default::default();
        for &(pid, sq) in &pos {
            map.entry(pid).or_default().push(sq);
        }

        // 1) Check White King (0x00) on e1= square  (rank=0, file=4) = 0*8+4 = 4
        assert_eq!(map.get(&0x00), Some(&vec![encode_square(0, 4)]));

        // 2) White Queen (0x04) on d1 (rank=0,file=3) = 3
        assert_eq!(map.get(&0x04), Some(&vec![encode_square(0, 3)]));

        // 3) White Rooks: 
        //    east rook = 0x03 at h1=7; 
        //    west rook = 0x07 at a1=0
        let mut w_rooks = map.get(&0x03).unwrap().clone();
        w_rooks.sort();
        assert_eq!(w_rooks, vec![encode_square(0, 7)]);
        assert_eq!(map.get(&0x07), Some(&vec![encode_square(0, 0)]));

        // 4) White Knights: 
        //    east (g1=6) = 0x02, 
        //    west (b1=1) = 0x06
        assert_eq!(map.get(&0x02), Some(&vec![encode_square(0, 6)]));
        assert_eq!(map.get(&0x06), Some(&vec![encode_square(0, 1)]));

        // 5) White Bishops: 
        //    east (f1=5) = 0x01, 
        //    west (c1=2) = 0x05
        assert_eq!(map.get(&0x01), Some(&vec![encode_square(0, 5)]));
        assert_eq!(map.get(&0x05), Some(&vec![encode_square(0, 2)]));

        // 6) White Pawns (0x09) on rank=1, files=0..7 => squares 8..15
        let wpawn_sqs: Vec<u8> = (0..8).map(|f| encode_square(1, f)).collect();
        let mut got_wpawn_sqs = map.get(&0x09).unwrap().clone();
        got_wpawn_sqs.sort();
        assert_eq!(got_wpawn_sqs, wpawn_sqs);

        // 7) Black Pawns (0x1E) on rank=6, files=0..7 => squares 48..55
        let bpawn_sqs: Vec<u8> = (0..8).map(|f| encode_square(6, f)).collect();
        let mut got_bpawn_sqs = map.get(&0x1E).unwrap().clone();
        got_bpawn_sqs.sort();
        assert_eq!(got_bpawn_sqs, bpawn_sqs);

        // 8) Black back rank:
        //    King (0x10) on e8= (7<<3)|4 = 60
        assert_eq!(map.get(&0x10), Some(&vec![encode_square(7, 4)]));
        //    Queen (0x14) on d8 = 59
        assert_eq!(map.get(&0x14), Some(&vec![encode_square(7, 3)]));
        //    Rooks: east 0x13 at h8=63; west 0x17 at a8=56
        assert_eq!(map.get(&0x13), Some(&vec![encode_square(7, 7)]));
        assert_eq!(map.get(&0x17), Some(&vec![encode_square(7, 0)]));
        //    Knights: east 0x12 at g8=62; west 0x16 at b8=57
        assert_eq!(map.get(&0x12), Some(&vec![encode_square(7, 6)]));
        assert_eq!(map.get(&0x16), Some(&vec![encode_square(7, 1)]));
        //    Bishops: east 0x11 at f8=61; west 0x15 at c8=58
        assert_eq!(map.get(&0x11), Some(&vec![encode_square(7, 5)]));
        assert_eq!(map.get(&0x15), Some(&vec![encode_square(7, 2)]));
    }
}