kish 1.1.7

A high-performance Turkish Draughts (Dama) engine with bitboard representation
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
//! Bitboard state representation for Turkish Draughts.
//!
//! This module defines the [`State`] struct which stores the complete board position
//! using efficient bitboard representation.
//!
//! # Bitboard Representation
//!
//! A bitboard uses a 64-bit integer where each bit represents one square.
//! Bit 0 = A1, Bit 7 = H1, Bit 56 = A8, Bit 63 = H8.
//!
//! The state consists of three bitboards:
//! - `pieces[0]`: White pieces (men and kings)
//! - `pieces[1]`: Black pieces (men and kings)
//! - `kings`: King pieces (subset of both colors)
//!
//! # Bitboard Operations
//!
//! Common bitboard operations used in this engine:
//!
//! | Operation | Description |
//! |-----------|-------------|
//! | `mask << 8` | Shift up one row |
//! | `mask >> 8` | Shift down one row |
//! | `mask << 1` | Shift right one column (watch for wrap) |
//! | `mask >> 1` | Shift left one column (watch for wrap) |
//! | `a & b` | Intersection of positions |
//! | `a \| b` | Union of positions |
//! | `a ^ b` | Toggle positions (XOR) |
//! | `!a` | Complement (all other squares) |
//!
//! # Example
//!
//! ```rust
//! use kish::{State, Square};
//!
//! // Create initial position
//! let state = State::default();
//!
//! // White has a piece on A2
//! assert_ne!(state.pieces[0] & Square::A2.to_mask(), 0);
//!
//! // Black has a piece on A7
//! assert_ne!(state.pieces[1] & Square::A7.to_mask(), 0);
//!
//! // No kings at start
//! assert_eq!(state.kings, 0);
//! ```
//!
//! # XOR-Based State Updates
//!
//! State changes are applied using XOR operations for efficiency:
//!
//! ```rust
//! use kish::{State, Square};
//!
//! let mut state = State::zeros();
//! state.pieces[0] = Square::D4.to_mask();
//!
//! // Create a delta that moves D4 to D5
//! let delta = State::new(
//!     [Square::D4.to_mask() | Square::D5.to_mask(), 0],
//!     0,
//! );
//!
//! state.apply_(&delta);
//! assert_eq!(state.pieces[0], Square::D5.to_mask());
//! ```

use std::fmt;

use crate::Team;

// Row masks (internal use only, complete set for maintainability)
pub(crate) const MASK_ROW_1: u64 = 0x0000_0000_0000_00FF;
pub(crate) const MASK_ROW_2: u64 = 0x0000_0000_0000_FF00;
#[allow(dead_code)]
pub(crate) const MASK_ROW_3: u64 = 0x0000_0000_00FF_0000;
#[allow(dead_code)]
pub(crate) const MASK_ROW_4: u64 = 0x0000_0000_FF00_0000;
#[allow(dead_code)]
pub(crate) const MASK_ROW_5: u64 = 0x0000_00FF_0000_0000;
#[allow(dead_code)]
pub(crate) const MASK_ROW_6: u64 = 0x0000_FF00_0000_0000;
pub(crate) const MASK_ROW_7: u64 = 0x00FF_0000_0000_0000;
pub(crate) const MASK_ROW_8: u64 = 0xFF00_0000_0000_0000;

// Column masks (internal use only, complete set for maintainability)
pub(crate) const MASK_COL_A: u64 = 0x0101_0101_0101_0101;
pub(crate) const MASK_COL_B: u64 = 0x0202_0202_0202_0202;
#[allow(dead_code)]
pub(crate) const MASK_COL_C: u64 = 0x0404_0404_0404_0404;
#[allow(dead_code)]
pub(crate) const MASK_COL_D: u64 = 0x0808_0808_0808_0808;
#[allow(dead_code)]
pub(crate) const MASK_COL_E: u64 = 0x1010_1010_1010_1010;
#[allow(dead_code)]
pub(crate) const MASK_COL_F: u64 = 0x2020_2020_2020_2020;
pub(crate) const MASK_COL_G: u64 = 0x4040_4040_4040_4040;
pub(crate) const MASK_COL_H: u64 = 0x8080_8080_8080_8080;

// Promotion row masks indexed by team
pub(crate) const MASK_ROW_PROMOTIONS: [u64; 2] = [MASK_ROW_8, MASK_ROW_1];

/// The complete board state using bitboard representation.
///
/// This struct stores piece positions for both teams and king status
/// using three 64-bit integers (24 bytes total).
///
/// # Fields
///
/// - `pieces[0]`: Bitboard of all White pieces
/// - `pieces[1]`: Bitboard of all Black pieces
/// - `kings`: Bitboard of all king pieces (subset of `pieces[0] | pieces[1]`)
///
/// # Invariants
///
/// - `pieces[0] & pieces[1] == 0` (no square has both colors)
/// - `kings & (pieces[0] | pieces[1]) == kings` (kings must be on a piece)
/// - Each team has at most 16 pieces
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct State {
    /// Bitboard of pieces for each team: `pieces[0]` = White, `pieces[1]` = Black.
    pub pieces: [u64; 2],
    /// Bitboard of king pieces (men that have been promoted).
    pub kings: u64,
}

impl State {
    /// Creates a new state.
    #[must_use]
    pub const fn new(pieces: [u64; 2], kings: u64) -> Self {
        Self { pieces, kings }
    }

    /// Creates a new state with default configuration.
    #[must_use]
    pub const fn default() -> Self {
        let whites = MASK_ROW_2 | MASK_ROW_3;
        let blacks = MASK_ROW_6 | MASK_ROW_7;
        Self {
            pieces: [whites, blacks],
            kings: 0u64,
        }
    }

    /// Creates a new state with zeros.
    #[must_use]
    pub const fn zeros() -> Self {
        Self {
            pieces: [0u64, 0u64],
            kings: 0u64,
        }
    }
}

impl State {
    /// Validates the state invariants in debug builds.
    ///
    /// This method uses `debug_assert!` for performance, so checks are only
    /// performed in debug builds. In release builds, this is a no-op.
    ///
    /// # Panics (debug builds only)
    ///
    /// Panics if any invariant is violated:
    /// - A square is occupied by both teams
    /// - A king exists on an empty square
    /// - A team has more than 16 pieces
    ///
    /// # Note
    ///
    /// This does NOT validate that pieces on the promotion row are kings.
    /// During multi-capture sequences, pawns can temporarily be on the promotion
    /// row before being promoted at the end of the sequence. The promotion logic
    /// in `generate_pawn_captures_at` ensures pawns are promoted when sequences
    /// end on the promotion row.
    pub fn validate(&self) {
        debug_assert_eq!(
            self.pieces[0] & self.pieces[1],
            0,
            "a single square is occupied by both teams"
        );

        debug_assert_eq!(
            self.kings,
            self.kings & (self.pieces[0] | self.pieces[1]),
            "an empty square is marked as king"
        );

        for team_index in 0..=1 {
            let team = Team::from_usize(team_index);

            debug_assert!(
                self.pieces[team_index].count_ones() <= 16,
                "more than 16 pieces for {team}",
            );
        }
    }

    /// The bitboard representing the empty squares.
    #[inline(always)]
    #[must_use]
    pub const fn empty(&self) -> u64 {
        !(self.pieces[0] | self.pieces[1])
    }

    /// Applies a transformation to the state in-place.
    #[inline(always)]
    pub const fn apply_(&mut self, other: &Self) {
        self.pieces[0] ^= other.pieces[0];
        self.pieces[1] ^= other.pieces[1];
        self.kings ^= other.kings;
    }

    /// Returns a new state after applying the transformation.
    #[inline(always)]
    #[must_use]
    pub fn apply(&self, other: &Self) -> Self {
        let mut new_state = *self;
        new_state.apply_(other);
        new_state
    }

    /// Rotates the state by 180 degrees in-place.
    #[inline]
    pub const fn rotate_(&mut self) {
        let tmp_pieces = self.pieces[0];
        self.pieces[0] = self.pieces[1].reverse_bits();
        self.pieces[1] = tmp_pieces.reverse_bits();
        self.kings = self.kings.reverse_bits();
    }

    /// Returns a new state after rotating by 180 degrees.
    #[inline]
    #[must_use]
    pub fn rotate(&self) -> Self {
        let mut new_state = *self;
        new_state.rotate_();
        new_state
    }
}

impl Default for State {
    /// Returns the initial game position.
    ///
    /// This is equivalent to [`State::default()`](State::default).
    fn default() -> Self {
        Self::default()
    }
}

impl fmt::Display for State {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Add A-H column labels at the top
        let mut result = "   A B C D E F G H\n".to_string();

        for row in (0..8).rev() {
            // Add 1-8 row labels on the left
            let mut line = format!("{} ", row + 1);

            for col in 0..8 {
                let index: u8 = row * 8 + col;
                let mut ch = '.';

                if (self.pieces[0] >> index) & 1u64 == 1u64 {
                    ch = 'w';
                } else if (self.pieces[1] >> index) & 1u64 == 1u64 {
                    ch = 'b';
                }

                if (self.kings >> index) & 1u64 == 1u64 {
                    ch = ch.to_ascii_uppercase();
                }

                line += &format!(" {ch}");
            }

            // Add 1-8 row labels on the right
            line += &format!("  {}\n", row + 1);

            result += &line;
        }

        // Add A-H column labels at the bottom
        result += "   A B C D E F G H";

        write!(f, "{result}")
    }
}

#[cfg(test)]
mod tests {

    use crate::Square;

    use super::*;

    #[test]
    fn new() {
        let whites = MASK_ROW_2;
        let blacks = MASK_ROW_7;
        let kings = MASK_ROW_2;
        let state = State::new([whites, blacks], kings);
        assert_eq!(state.pieces[Team::White.to_usize()], whites);
        assert_eq!(state.pieces[Team::Black.to_usize()], blacks);
        assert_eq!(state.kings, kings);
    }

    #[test]
    fn default() {
        let state = State::default();
        assert_eq!(
            state.pieces[Team::White.to_usize()],
            MASK_ROW_2 | MASK_ROW_3
        );
        assert_eq!(
            state.pieces[Team::Black.to_usize()],
            MASK_ROW_6 | MASK_ROW_7
        );
        assert_eq!(state.kings, 0u64);
    }

    #[test]
    fn default_trait() {
        // Test that the Default trait implementation matches the inherent method
        let state_inherent = State::default();
        let state_trait: State = Default::default();
        assert_eq!(state_inherent, state_trait);
    }

    #[test]
    fn zeros() {
        let state = State::zeros();
        assert_eq!(state.pieces[Team::White.to_usize()], 0u64);
        assert_eq!(state.pieces[Team::Black.to_usize()], 0u64);
        assert_eq!(state.kings, 0u64);
    }

    #[test]
    fn validate_default() {
        let state = State::default();
        state.validate();
    }

    #[test]
    #[should_panic(expected = "a single square is occupied by both teams")]
    fn invalid_validate_common_pieces() {
        let whites = MASK_ROW_2 | MASK_ROW_3;
        let blacks = MASK_ROW_3 | MASK_ROW_4;
        let kings = 0u64;
        let state = State::new([whites, blacks], kings);
        state.validate();
    }

    #[test]
    #[should_panic(expected = "an empty square is marked as king")]
    fn invalid_validate_empty_square_as_king() {
        let whites = MASK_ROW_2;
        let blacks = MASK_ROW_3;
        let kings = MASK_ROW_1 | MASK_ROW_2;
        let state = State::new([whites, blacks], kings);
        state.validate();
    }

    #[test]
    #[should_panic(expected = "more than 16 pieces for White")]
    fn invalid_validate_more_than_16_white_pieces() {
        let whites = MASK_ROW_2 | MASK_ROW_3 | Square::A4.to_mask();
        let blacks = MASK_ROW_7;
        let kings = 0u64;
        let state = State::new([whites, blacks], kings);
        state.validate();
    }

    #[test]
    #[should_panic(expected = "more than 16 pieces for Black")]
    fn invalid_validate_more_than_16_black_pieces() {
        let whites = MASK_ROW_2;
        let blacks = MASK_ROW_6 | MASK_ROW_7 | Square::A5.to_mask();
        let kings = 0u64;
        let state = State::new([whites, blacks], kings);
        state.validate();
    }

    // Note: Removed invalid_validate_white_promotion and invalid_validate_black_promotion
    // tests. Pawns CAN temporarily be on the promotion row during multi-capture sequences.
    // Promotion is added at the end of the sequence by generate_pawn_captures_at.

    #[test]
    fn empty() {
        let state = State::default();
        assert_eq!(
            state.empty(),
            !(MASK_ROW_2 | MASK_ROW_3 | MASK_ROW_6 | MASK_ROW_7)
        );
    }

    #[test]
    fn apply() {
        let state = State::default();
        let transformation = State::new([MASK_ROW_2, MASK_ROW_7], MASK_ROW_3); // del row 2&7, promote row 3
        let new_state = state.apply(&transformation);
        assert_eq!(new_state.pieces[Team::White.to_usize()], MASK_ROW_3);
        assert_eq!(new_state.pieces[Team::Black.to_usize()], MASK_ROW_6);
        assert_eq!(new_state.kings, MASK_ROW_3);

        // Test in-place
        let mut new_state_ = state;
        new_state_.apply_(&transformation);
        assert_eq!(new_state_, new_state);
    }

    #[test]
    fn rotate() {
        let state = State::new(
            [
                MASK_ROW_2 | Square::B3.to_mask(),
                MASK_ROW_6 | Square::F5.to_mask(),
            ],
            Square::B3.to_mask() | Square::F5.to_mask(),
        );
        let expected = State::new(
            [
                MASK_ROW_3 | Square::C4.to_mask(),
                MASK_ROW_7 | Square::G6.to_mask(),
            ],
            Square::G6.to_mask() | Square::C4.to_mask(),
        );
        let new_state = state.rotate();
        assert_eq!(new_state, expected);

        // Test in-place
        let mut new_state_ = state;
        new_state_.rotate_();
        assert_eq!(new_state_, new_state);
    }

    #[test]
    fn fmt() {
        let state = State::default();
        let expected = "   A B C D E F G H\n8  . . . . . . . .  8\n7  b b b b b b b b  7\n6  b b b b b b b b  6\n5  . . . . . . . .  5\n4  . . . . . . . .  4\n3  w w w w w w w w  3\n2  w w w w w w w w  2\n1  . . . . . . . .  1\n   A B C D E F G H";
        let result = format!("{state}");
        assert_eq!(result, expected);
    }

    #[test]
    fn fmt_with_kings() {
        let state = State::new(
            [Square::A1.to_mask(), Square::H8.to_mask()],
            Square::A1.to_mask() | Square::H8.to_mask(),
        );
        let result = format!("{state}");
        assert!(result.contains('W'), "White king should display as 'W'");
        assert!(result.contains('B'), "Black king should display as 'B'");
    }
}