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
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
//! Full game management with history tracking for Turkish Draughts.
//!
//! This module provides the [`Game`] struct which wraps [`Board`] and adds:
//! - Position history tracking for threefold repetition detection
//! - Move counting for the insufficient progress rule
//!
//! # When to Use Game vs Board
//!
//! Use [`Board`] directly when:
//! - Performing AI search (alpha-beta, MCTS, etc.)
//! - Running perft tests
//! - You don't need draw detection beyond the basic 1v1 rule
//!
//! Use [`Game`] when:
//! - Playing a full game with proper draw detection
//! - You need to track game history
//! - You want to implement undo functionality
//!
//! # Example
//!
//! ```rust
//! use kish::{Game, GameStatus};
//!
//! let mut game = Game::new();
//!
//! // Play moves
//! let actions = game.actions();
//! if let Some(action) = actions.first() {
//!     game.make_move(action);
//! }
//!
//! // Check status (includes threefold repetition)
//! match game.status() {
//!     GameStatus::InProgress => println!("Game continues"),
//!     GameStatus::Draw => println!("Draw!"),
//!     GameStatus::Won(team) => println!("{} wins!", team),
//! }
//!
//! // Undo the last move
//! game.undo_move();
//! ```
//!
//! # Threefold Repetition (Rule 9.2)
//!
//! A draw is declared when the same position occurs three times with the same
//! player to move. The positions do not need to be consecutive.
//!
//! ```rust
//! use kish::{Game, Board, Team, Square, GameStatus};
//!
//! // Create a position where repetition can occur
//! let board = Board::from_squares(
//!     Team::White,
//!     &[Square::D4],
//!     &[Square::E5],
//!     &[Square::D4, Square::E5], // Both are kings
//! );
//! let mut game = Game::from_board(board);
//!
//! // The game tracks positions as moves are made
//! // Threefold repetition is automatically detected
//! ```
//!
//! # Insufficient Progress (Rule 9.4)
//!
//! A draw is declared after 150 consecutive plies (75 full moves) without any
//! capture. This prevents indefinitely prolonged endgames.

use rustc_hash::FxHashMap;

use crate::{Action, Board, GameStatus, Team};

/// Hash type for position lookup (single u64 for fast hashing).
type PositionHash = u64;

/// Number of half-moves (plies) without a capture before a draw is declared.
///
/// Per Rule 9.4, a draw occurs after this many consecutive plies without capture.
/// Set to 150 plies (75 full moves).
const INSUFFICIENT_PROGRESS_THRESHOLD: u16 = 150;

/// A full game with history tracking for proper draw detection.
///
/// This struct wraps a [`Board`] and maintains:
/// - A count of each position occurrence (for threefold repetition)
/// - A counter of half-moves since last capture (for insufficient progress)
/// - A history stack for undo functionality
///
/// # Memory Usage
///
/// The position history uses a `FxHashMap` which grows with unique positions.
/// For typical games, this is negligible. For very long games or analysis,
/// consider periodically clearing irrelevant history.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
    /// The current board state.
    board: Board,
    /// Count of each position occurrence for threefold repetition detection.
    position_counts: FxHashMap<PositionHash, u8>,
    /// Number of half-moves since last capture.
    /// A draw is declared at [`INSUFFICIENT_PROGRESS_THRESHOLD`] half-moves.
    halfmove_clock: u16,
    /// History stack for undo: (action, previous halfmove_clock, was_capture).
    history: Vec<(Action, u16, bool)>,
}

/// Typical game length for pre-allocation (most games end within 100 moves).
const TYPICAL_GAME_LENGTH: usize = 100;

impl Game {
    /// Creates a new game with the standard starting position.
    #[must_use]
    pub fn new() -> Self {
        let board = Board::new_default();
        let mut game = Self {
            board,
            position_counts: FxHashMap::with_capacity_and_hasher(
                TYPICAL_GAME_LENGTH,
                Default::default(),
            ),
            halfmove_clock: 0,
            history: Vec::with_capacity(TYPICAL_GAME_LENGTH),
        };
        game.record_position();
        game
    }

    /// Creates a game from an existing board position.
    ///
    /// The halfmove clock starts at 0 and the provided position is recorded
    /// as the first occurrence.
    #[must_use]
    pub fn from_board(board: Board) -> Self {
        let mut game = Self {
            board,
            position_counts: FxHashMap::with_capacity_and_hasher(
                TYPICAL_GAME_LENGTH,
                Default::default(),
            ),
            halfmove_clock: 0,
            history: Vec::with_capacity(TYPICAL_GAME_LENGTH),
        };
        game.record_position();
        game
    }

    /// Returns a reference to the current board.
    #[inline]
    #[must_use]
    pub const fn board(&self) -> &Board {
        &self.board
    }

    /// Returns the current team's turn.
    #[inline]
    #[must_use]
    pub const fn turn(&self) -> Team {
        self.board.turn
    }

    /// Returns the number of half-moves since the last capture.
    #[inline]
    #[must_use]
    pub const fn halfmove_clock(&self) -> u16 {
        self.halfmove_clock
    }

    /// Returns the number of moves in the game history.
    #[inline]
    #[must_use]
    pub fn move_count(&self) -> usize {
        self.history.len()
    }

    /// Returns all legal actions from the current position.
    #[inline]
    #[must_use]
    pub fn actions(&self) -> Vec<Action> {
        self.board.actions()
    }

    /// Computes the current game status, including draw conditions.
    ///
    /// This checks:
    /// 1. Win by capturing all pieces
    /// 2. Win by blocking the opponent
    /// 3. Draw by 1v1 (Rule 9.3)
    /// 4. Draw by threefold repetition (Rule 9.2)
    /// 5. Draw by insufficient progress (Rule 9.4) - 150 plies without capture
    #[must_use]
    pub fn status(&self) -> GameStatus {
        // First check basic status (win/loss/1v1 draw)
        let basic_status = self.board.status();
        if basic_status != GameStatus::InProgress {
            return basic_status;
        }

        // Check threefold repetition
        if self.is_threefold_repetition() {
            return GameStatus::Draw;
        }

        // Check insufficient progress
        if self.halfmove_clock >= INSUFFICIENT_PROGRESS_THRESHOLD {
            return GameStatus::Draw;
        }

        GameStatus::InProgress
    }

    /// Makes a move, updating the board and game history.
    ///
    /// This method:
    /// 1. Applies the action to the board
    /// 2. Swaps the turn
    /// 3. Records the new position for repetition detection
    /// 4. Updates the halfmove clock (resets on capture)
    /// 5. Pushes to history for undo support
    #[inline]
    pub fn make_move(&mut self, action: &Action) {
        let is_capture = self.is_capture_action(action);

        // Save current state for undo
        let prev_halfmove = self.halfmove_clock;

        // Apply the action
        self.board.apply_(action);
        self.board.swap_turn_();

        // Update halfmove clock (only captures reset)
        if is_capture {
            self.halfmove_clock = 0;
        } else {
            self.halfmove_clock += 1;
        }

        // Record position for repetition detection
        self.record_position();

        // Push to history
        self.history.push((*action, prev_halfmove, is_capture));
    }

    /// Undoes the last move, restoring the previous board state.
    ///
    /// Returns `true` if a move was undone, `false` if there was no move to undo.
    #[inline]
    pub fn undo_move(&mut self) -> bool {
        if let Some((action, prev_halfmove, _)) = self.history.pop() {
            // Decrement position count before undoing
            self.decrement_position_count();

            // Undo the turn swap
            self.board.swap_turn_();

            // Undo the action (XOR is self-inverse)
            self.board.apply_(&action);

            // Restore halfmove clock
            self.halfmove_clock = prev_halfmove;

            true
        } else {
            false
        }
    }

    /// Checks if the current position has occurred three times.
    #[inline]
    #[must_use]
    pub fn is_threefold_repetition(&self) -> bool {
        let hash = self.position_hash();
        self.position_counts.get(&hash).copied().unwrap_or(0) >= 3
    }

    /// Returns the number of times the current position has occurred.
    #[inline]
    #[must_use]
    pub fn position_occurrence_count(&self) -> u8 {
        let hash = self.position_hash();
        self.position_counts.get(&hash).copied().unwrap_or(0)
    }

    /// Clears the position history and resets the halfmove clock.
    ///
    /// This is useful when starting a new game from a position
    /// where prior history should not count.
    pub fn clear_history(&mut self) {
        self.position_counts.clear();
        self.history.clear();
        self.halfmove_clock = 0;
        self.record_position();
    }

    /// Perft (performance test) - counts leaf nodes at a given depth.
    ///
    /// This is similar to [`Board::perft`] but uses `make_move`/`undo_move`,
    /// which is useful for verifying that the Game's move/undo cycle is correct.
    ///
    /// # Note
    ///
    /// This is slower than `Board::perft` due to history tracking overhead.
    /// Use `Board::perft` for performance testing move generation.
    ///
    /// # Example
    ///
    /// ```rust
    /// use kish::Game;
    ///
    /// let mut game = Game::new();
    /// let nodes = game.perft(3);
    /// println!("Positions at depth 3: {}", nodes);
    /// ```
    pub fn perft(&mut self, depth: u64) -> u64 {
        if depth == 0 {
            return 1;
        }

        let actions = self.actions();
        if actions.is_empty() {
            return 1; // Terminal node counts as 1
        }

        let mut nodes = 0u64;
        for action in &actions {
            self.make_move(action);
            nodes += self.perft(depth - 1);
            self.undo_move();
        }
        nodes
    }

    // =========================================================================
    // Private helpers
    // =========================================================================

    /// Computes a hash for the current position (state + turn).
    #[inline]
    fn position_hash(&self) -> PositionHash {
        // XOR mixing with golden ratio constants for good distribution
        let mut hash = self.board.state.pieces[0];
        hash ^= self.board.state.pieces[1].wrapping_mul(0x9e37_79b9_7f4a_7c15);
        hash ^= self.board.state.kings.wrapping_mul(0x517c_c1b7_2722_0a95);
        hash ^= (self.board.turn.to_usize() as u64).wrapping_mul(0x2545_f491_4f6c_dd1d);
        hash
    }

    /// Records the current position in the occurrence map.
    #[inline]
    fn record_position(&mut self) {
        let hash = self.position_hash();
        *self.position_counts.entry(hash).or_insert(0) += 1;
    }

    /// Decrements the position count for the current position.
    #[inline]
    fn decrement_position_count(&mut self) {
        let hash = self.position_hash();
        if let Some(count) = self.position_counts.get_mut(&hash) {
            *count = count.saturating_sub(1);
            if *count == 0 {
                self.position_counts.remove(&hash);
            }
        }
    }

    /// Checks if an action is a capture (removes opponent pieces).
    #[inline]
    fn is_capture_action(&self, action: &Action) -> bool {
        let opponent_index = self.board.turn.opponent().to_usize();
        action.delta.pieces[opponent_index] != 0
    }
}

impl Default for Game {
    fn default() -> Self {
        Self::new()
    }
}

impl From<Board> for Game {
    fn from(board: Board) -> Self {
        Self::from_board(board)
    }
}

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

    #[test]
    fn new_game_starts_at_default_position() {
        let game = Game::new();
        assert_eq!(game.board, Board::new_default());
        assert_eq!(game.turn(), Team::White);
        assert_eq!(game.halfmove_clock(), 0);
        assert_eq!(game.move_count(), 0);
    }

    #[test]
    fn from_board_preserves_position() {
        let board = Board::from_squares(
            Team::Black,
            &[Square::A2, Square::B2],
            &[Square::G7, Square::H7],
            &[],
        );
        let game = Game::from_board(board);
        assert_eq!(game.board, board);
        assert_eq!(game.turn(), Team::Black);
    }

    #[test]
    fn make_move_updates_board() {
        let mut game = Game::new();
        let actions = game.actions();
        assert!(!actions.is_empty());

        let action = actions[0];
        game.make_move(&action);

        // Turn should have swapped
        assert_eq!(game.turn(), Team::Black);
        assert_eq!(game.move_count(), 1);
    }

    #[test]
    fn undo_move_restores_board() {
        let mut game = Game::new();
        let original_board = game.board;

        let actions = game.actions();
        game.make_move(&actions[0]);
        assert_ne!(game.board, original_board);

        game.undo_move();
        assert_eq!(game.board, original_board);
        assert_eq!(game.move_count(), 0);
    }

    #[test]
    fn halfmove_clock_increments_for_king_moves() {
        // Create a position with only kings
        let board = Board::from_squares(
            Team::White,
            &[Square::D4],
            &[Square::E6],
            &[Square::D4, Square::E6], // Both are kings
        );
        let mut game = Game::from_board(board);

        // Make a king move (should increment clock)
        let actions = game.actions();
        game.make_move(&actions[0]);
        assert_eq!(game.halfmove_clock(), 1);
    }

    #[test]
    fn halfmove_clock_resets_on_capture() {
        // Create a position where capture is forced - use a pawn, not a king
        // Pawn captures are simpler (single landing square)
        let board = Board::from_squares(
            Team::White,
            &[Square::D4],
            &[Square::D5, Square::H8], // Need second piece to avoid 1v1 draw
            &[],                       // No kings
        );
        let mut game = Game::from_board(board);

        // Set clock to non-zero
        game.halfmove_clock = 10;

        // Make the capture (should reset clock)
        let actions = game.actions();
        assert_eq!(actions.len(), 1); // Only one capture possible (pawn lands on D6)
        game.make_move(&actions[0]);
        assert_eq!(game.halfmove_clock(), 0);
    }

    #[test]
    fn threefold_repetition_detection() {
        // Create a position with two kings each, far enough apart to avoid 1v1 draw.
        // Kings at A1/B1 for white, H7/H8 for black.
        // We'll shuffle A1<->A2 and H8<->G8 to create repetitions.
        let board = Board::from_squares(
            Team::White,
            &[Square::A1, Square::B1],
            &[Square::H7, Square::H8],
            &[Square::A1, Square::B1, Square::H7, Square::H8],
        );
        let mut game = Game::from_board(board);

        // Initial position is recorded once
        assert_eq!(game.position_occurrence_count(), 1);
        assert!(!game.is_threefold_repetition());

        // Find moves for the shuffle pattern.
        // White: A1 -> A2, Black: H8 -> G8, White: A2 -> A1, Black: G8 -> H8
        // This returns to the starting position.

        // Helper to find a specific move
        let find_move = |g: &Game, from: Square, to: Square| -> Action {
            g.actions()
                .into_iter()
                .find(|a| {
                    let from_mask = from.to_mask();
                    let to_mask = to.to_mask();
                    let friendly_idx = g.turn().to_usize();
                    let delta = a.delta.pieces[friendly_idx];
                    (delta & from_mask) != 0 && (delta & to_mask) != 0
                })
                .expect("Move not found")
        };

        // Round 1: back to start
        game.make_move(&find_move(&game, Square::A1, Square::A2)); // White A1->A2
        game.make_move(&find_move(&game, Square::H8, Square::G8)); // Black H8->G8
        game.make_move(&find_move(&game, Square::A2, Square::A1)); // White A2->A1
        game.make_move(&find_move(&game, Square::G8, Square::H8)); // Black G8->H8
        assert_eq!(game.position_occurrence_count(), 2);
        assert!(!game.is_threefold_repetition());

        // Round 2: back to start again
        game.make_move(&find_move(&game, Square::A1, Square::A2));
        game.make_move(&find_move(&game, Square::H8, Square::G8));
        game.make_move(&find_move(&game, Square::A2, Square::A1));
        game.make_move(&find_move(&game, Square::G8, Square::H8));
        assert_eq!(game.position_occurrence_count(), 3);
        assert!(game.is_threefold_repetition());
        assert_eq!(game.status(), GameStatus::Draw);
    }

    #[test]
    fn insufficient_progress_draw() {
        // Need more than 1 piece each to avoid 1v1 draw rule
        let board = Board::from_squares(
            Team::White,
            &[Square::D4, Square::A1],
            &[Square::E6, Square::H8],
            &[Square::D4, Square::A1, Square::E6, Square::H8], // All are kings
        );
        let mut game = Game::from_board(board);

        // Set clock to threshold - 1 (one short of draw)
        game.halfmove_clock = INSUFFICIENT_PROGRESS_THRESHOLD - 1;
        assert_eq!(game.status(), GameStatus::InProgress);

        // Set clock to threshold (draw)
        game.halfmove_clock = INSUFFICIENT_PROGRESS_THRESHOLD;
        assert_eq!(game.status(), GameStatus::Draw);
    }

    #[test]
    fn status_checks_all_conditions() {
        // Test 1v1 draw takes priority
        let board = Board::from_squares(Team::White, &[Square::A1], &[Square::H8], &[]);
        let game = Game::from_board(board);
        assert_eq!(game.status(), GameStatus::Draw);
    }

    #[test]
    fn clear_history_resets_everything() {
        let mut game = Game::new();
        let actions = game.actions();
        game.make_move(&actions[0]);
        game.halfmove_clock = 50;

        game.clear_history();

        assert_eq!(game.halfmove_clock(), 0);
        assert_eq!(game.move_count(), 0);
        assert_eq!(game.position_occurrence_count(), 1);
    }

    #[test]
    fn position_count_decrements_on_undo() {
        let mut game = Game::new();
        let initial_count = game.position_occurrence_count();

        let actions = game.actions();
        game.make_move(&actions[0]);

        // Verify undo decrements counts
        game.undo_move();
        assert_eq!(game.position_occurrence_count(), initial_count);
    }

    #[test]
    fn from_board_trait_impl() {
        let board = Board::from_squares(Team::Black, &[Square::C3], &[Square::F6], &[Square::C3]);
        let game: Game = board.into();
        assert_eq!(game.turn(), Team::Black);
        assert_eq!(game.halfmove_clock(), 0);
        assert_eq!(game.position_occurrence_count(), 1);
    }

    #[test]
    fn undo_move_returns_false_when_empty() {
        let mut game = Game::new();
        assert!(!game.undo_move());
    }

    #[test]
    fn default_trait_impl() {
        let game1 = Game::default();
        let game2 = Game::new();
        assert_eq!(game1, game2);
    }

    #[test]
    fn halfmove_clock_does_not_reset_on_pawn_move() {
        // Create position with a pawn that can move (no capture)
        let board = Board::from_squares(
            Team::White,
            &[Square::D4, Square::A1], // D4 is a pawn, A1 is a king
            &[Square::H7, Square::H8],
            &[Square::A1, Square::H7, Square::H8], // Only A1, H7, H8 are kings
        );
        let mut game = Game::from_board(board);
        game.halfmove_clock = 10;

        // Find the pawn move (D4 -> D5)
        let pawn_move = game
            .actions()
            .into_iter()
            .find(|a| {
                let d4 = Square::D4.to_mask();
                let d5 = Square::D5.to_mask();
                (a.delta.pieces[0] & d4) != 0 && (a.delta.pieces[0] & d5) != 0
            })
            .expect("Pawn move not found");

        game.make_move(&pawn_move);
        // Pawn moves do NOT reset clock (only captures do per Rule 9.4)
        assert_eq!(game.halfmove_clock(), 11);
    }

    #[test]
    fn insufficient_progress_threshold_is_configurable() {
        // Verify the const is used correctly
        assert_eq!(INSUFFICIENT_PROGRESS_THRESHOLD, 150);
    }

    #[test]
    fn perft_matches_board_perft() {
        // Verify Game::perft produces same results as Board::perft
        let board = Board::new_default();
        let mut game = Game::new();

        for depth in 0..=5 {
            let board_result = board.perft(depth);
            let game_result = game.perft(depth);
            assert_eq!(
                board_result, game_result,
                "Game::perft({}) = {} != Board::perft({}) = {}",
                depth, game_result, depth, board_result
            );
        }
    }

    #[test]
    fn perft_state_unchanged_after_call() {
        // Verify the game state is unchanged after perft
        let mut game = Game::new();
        let original_board = game.board;
        let original_count = game.move_count();

        game.perft(3);

        assert_eq!(game.board, original_board);
        assert_eq!(game.move_count(), original_count);
    }
}