Skip to main content

quantik_core/
moves.rs

1use crate::bitboard::Bitboard;
2use crate::constants::{MAX_PIECES_PER_SHAPE, NUM_SHAPES, WIN_MASKS};
3use crate::game::current_player;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct Move {
7    pub player: u8,
8    pub shape: u8,
9    pub position: u8,
10}
11
12impl Move {
13    pub fn new(player: u8, shape: u8, position: u8) -> Self {
14        debug_assert!(player <= 1);
15        debug_assert!(shape < 4);
16        debug_assert!(position < 16);
17        Self {
18            player,
19            shape,
20            position,
21        }
22    }
23}
24
25/// Check whether `(player, shape, position)` is legal on `bb`.
26pub fn is_move_legal(bb: &Bitboard, player: u8, shape: u8, position: u8) -> bool {
27    if bb.is_position_occupied(position) {
28        return false;
29    }
30    if bb.shape_piece_count(player, shape) >= MAX_PIECES_PER_SHAPE as u32 {
31        return false;
32    }
33    let opponent = 1 - player;
34    let opp_bits = bb.planes[Bitboard::plane_index(opponent, shape)];
35    let pos_mask = 1u16 << position;
36    for &wm in &WIN_MASKS {
37        if (pos_mask & wm != 0) && (opp_bits & wm != 0) {
38            return false;
39        }
40    }
41    true
42}
43
44/// Generate all legal moves for the current player.
45pub fn generate_legal_moves(bb: &Bitboard) -> Vec<Move> {
46    let player = match current_player(bb) {
47        Some(p) => p,
48        None => return Vec::new(),
49    };
50    let occupied = bb.occupied();
51    let mut moves = Vec::new();
52
53    for shape in 0..NUM_SHAPES as u8 {
54        if bb.shape_piece_count(player, shape) >= MAX_PIECES_PER_SHAPE as u32 {
55            continue;
56        }
57        let opp_bits = bb.planes[Bitboard::plane_index(1 - player, shape)];
58        for pos in 0..16u8 {
59            if occupied & (1u16 << pos) != 0 {
60                continue;
61            }
62            let pos_mask = 1u16 << pos;
63            let blocked = WIN_MASKS
64                .iter()
65                .any(|&wm| (pos_mask & wm != 0) && (opp_bits & wm != 0));
66            if !blocked {
67                moves.push(Move::new(player, shape, pos));
68            }
69        }
70    }
71    moves
72}
73
74/// Apply a move (assumes legality).
75#[inline]
76pub fn apply_move(bb: &Bitboard, mv: &Move) -> Bitboard {
77    bb.with_move(mv.player, mv.shape, mv.position)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn initial_legal_moves() {
86        let moves = generate_legal_moves(&Bitboard::EMPTY);
87        // 4 shapes * 16 positions = 64
88        assert_eq!(moves.len(), 64);
89        assert!(moves.iter().all(|m| m.player == 0));
90    }
91
92    #[test]
93    fn occupied_position_blocked() {
94        let bb = Bitboard::EMPTY.with_move(0, 0, 0);
95        assert!(!is_move_legal(&bb, 1, 1, 0));
96    }
97
98    #[test]
99    fn opponent_shape_on_line_blocked() {
100        // Player 0 places shape A at position 0 (row 0, zone top-left)
101        let bb = Bitboard::EMPTY.with_move(0, 0, 0);
102        // Player 1 cannot place shape A anywhere on row 0 (positions 1,2,3),
103        // column 0 (positions 4,8,12), or zone top-left (positions 1,4,5)
104        assert!(!is_move_legal(&bb, 1, 0, 1)); // same row
105        assert!(!is_move_legal(&bb, 1, 0, 4)); // same col & zone
106                                               // But player 1 can place shape A at position 10 (no shared line)
107        assert!(is_move_legal(&bb, 1, 0, 10));
108    }
109
110    #[test]
111    fn max_pieces_per_shape() {
112        let bb = Bitboard::EMPTY
113            .with_move(0, 0, 0)
114            .with_move(1, 1, 5)
115            .with_move(0, 0, 10);
116        // Player 0 already has 2 A-pieces
117        assert!(!is_move_legal(&bb, 0, 0, 15));
118        // But can still place B
119        assert!(is_move_legal(&bb, 0, 1, 15));
120    }
121}