lazychess 0.1.1

A fast, memory-efficient chess engine library for Rust
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
use crate::board::Board;
use crate::types::*;

const KNIGHT_DIRS: [(i32, i32); 8] = [
    (-2, -1), (-2, 1), (-1, -2), (-1, 2),
    (1, -2),  (1, 2),  (2, -1),  (2, 1),
];

const KING_DIRS: [(i32, i32); 8] = [
    (-1, -1), (-1, 0), (-1, 1),
    (0, -1),           (0, 1),
    (1, -1),  (1, 0),  (1, 1),
];

const BISHOP_DIRS: [(i32, i32); 4] = [(-1, -1), (-1, 1), (1, -1), (1, 1)];
const ROOK_DIRS: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];

#[inline(always)]
fn to_sq(rank: i32, file: i32) -> Option<Square> {
    if (0..8).contains(&rank) && (0..8).contains(&file) {
        Some((rank * 8 + file) as Square)
    } else {
        None
    }
}

/// Generates all pseudo-legal moves for the side to move.
///
/// *Pseudo-legal* means moves that respect each piece's movement rules but may
/// leave the king in check. Filtering for full legality is done in
/// [`generate_legal_moves`].
pub fn generate_pseudo_legal_moves(board: &Board) -> Vec<Move> {
    let mut moves: Vec<Move> = Vec::with_capacity(48);
    for sq in 0u8..64 {
        if let Some(p) = board.piece_at(sq)
            && p.color == board.side_to_move {
                match p.piece_type {
                    PieceType::Pawn => gen_pawn_moves(board, sq, &mut moves),
                    PieceType::Knight => gen_knight_moves(board, sq, &mut moves),
                    PieceType::Bishop => gen_sliding(board, sq, &BISHOP_DIRS, &mut moves),
                    PieceType::Rook => gen_sliding(board, sq, &ROOK_DIRS, &mut moves),
                    PieceType::Queen => {
                        gen_sliding(board, sq, &BISHOP_DIRS, &mut moves);
                        gen_sliding(board, sq, &ROOK_DIRS, &mut moves);
                    }
                    PieceType::King => gen_king_moves(board, sq, &mut moves),
                }
            }
    }
    moves
}

fn gen_pawn_moves(board: &Board, from: Square, moves: &mut Vec<Move>) {
    let color = board.side_to_move;
    let rank = rank_of(from) as i32;
    let file = file_of(from) as i32;

    // White pawns advance upward (+1 rank); Black advance downward (-1 rank).
    let (dir, start_rank, promo_rank) = match color {
        Color::White => (1i32, 1i32, 6i32),
        Color::Black => (-1i32, 6i32, 1i32),
    };

    let target_rank = rank + dir;

    // Single-square push
    if let Some(to) = to_sq(target_rank, file)
        && board.piece_at(to).is_none() {
            if rank == promo_rank {
                push_promotions(from, to, moves);
            } else {
                moves.push(Move::new(from, to));

                // Double push from the starting rank
                if rank == start_rank
                    && let Some(to2) = to_sq(rank + 2 * dir, file)
                        && board.piece_at(to2).is_none() {
                            moves.push(Move::with_flag(from, to2, MoveFlag::DoublePawnPush));
                        }
            }
        }

    // Diagonal captures (including en passant)
    for &df in &[-1i32, 1i32] {
        if let Some(to) = to_sq(target_rank, file + df) {
            let is_ep = board.en_passant == Some(to);
            let has_enemy = board
                .piece_at(to)
                .map(|p| p.color != color)
                .unwrap_or(false);

            if has_enemy || is_ep {
                if rank == promo_rank {
                    push_promotions(from, to, moves);
                } else if is_ep {
                    moves.push(Move::with_flag(from, to, MoveFlag::EnPassant));
                } else {
                    moves.push(Move::new(from, to));
                }
            }
        }
    }
}

/// Pushes all four promotion variants (Q, R, B, N) onto the move list.
#[inline]
fn push_promotions(from: Square, to: Square, moves: &mut Vec<Move>) {
    for &pt in &[
        PieceType::Queen,
        PieceType::Rook,
        PieceType::Bishop,
        PieceType::Knight,
    ] {
        moves.push(Move::with_flag(from, to, MoveFlag::Promotion(pt)));
    }
}

fn gen_knight_moves(board: &Board, from: Square, moves: &mut Vec<Move>) {
    let color = board.side_to_move;
    let rank = rank_of(from) as i32;
    let file = file_of(from) as i32;

    for &(dr, df) in &KNIGHT_DIRS {
        if let Some(to) = to_sq(rank + dr, file + df)
            && board.piece_at(to).map(|p| p.color != color).unwrap_or(true) {
                moves.push(Move::new(from, to));
            }
    }
}

fn gen_sliding(board: &Board, from: Square, dirs: &[(i32, i32)], moves: &mut Vec<Move>) {
    let color = board.side_to_move;
    let rank = rank_of(from) as i32;
    let file = file_of(from) as i32;

    for &(dr, df) in dirs {
        let (mut r, mut f) = (rank + dr, file + df);
        while (0..8).contains(&r) && (0..8).contains(&f) {
            let to = (r * 8 + f) as Square;
            match board.piece_at(to) {
                None => moves.push(Move::new(from, to)),
                Some(p) => {
                    if p.color != color {
                        moves.push(Move::new(from, to));
                    }
                    break;
                }
            }
            r += dr;
            f += df;
        }
    }
}

fn gen_king_moves(board: &Board, from: Square, moves: &mut Vec<Move>) {
    let color = board.side_to_move;
    let rank = rank_of(from) as i32;
    let file = file_of(from) as i32;

    for &(dr, df) in &KING_DIRS {
        if let Some(to) = to_sq(rank + dr, file + df)
            && board.piece_at(to).map(|p| p.color != color).unwrap_or(true) {
                moves.push(Move::new(from, to));
            }
    }

    gen_castling(board, from, color, moves);
}

fn gen_castling(board: &Board, king_sq: Square, color: Color, moves: &mut Vec<Move>) {
    match color {
        Color::White => {
            if king_sq != 4 {
                return; // King not on e1; castling rights are meaningless.
            }
            if board.castling_rights.white_kingside
                && board.piece_at(5).is_none()
                && board.piece_at(6).is_none()
            {
                moves.push(Move::with_flag(4, 6, MoveFlag::CastleKingside));
            }
            if board.castling_rights.white_queenside
                && board.piece_at(1).is_none()
                && board.piece_at(2).is_none()
                && board.piece_at(3).is_none()
            {
                moves.push(Move::with_flag(4, 2, MoveFlag::CastleQueenside));
            }
        }
        Color::Black => {
            if king_sq != 60 {
                return;
            }
            if board.castling_rights.black_kingside
                && board.piece_at(61).is_none()
                && board.piece_at(62).is_none()
            {
                moves.push(Move::with_flag(60, 62, MoveFlag::CastleKingside));
            }
            if board.castling_rights.black_queenside
                && board.piece_at(57).is_none()
                && board.piece_at(58).is_none()
                && board.piece_at(59).is_none()
            {
                moves.push(Move::with_flag(60, 58, MoveFlag::CastleQueenside));
            }
        }
    }
}

/// Returns `true` if `sq` is attacked by any piece belonging to `by_color`.
pub fn is_square_attacked(board: &Board, sq: Square, by_color: Color) -> bool {
    let rank = rank_of(sq) as i32;
    let file = file_of(sq) as i32;

    // Knight attacks
    for &(dr, df) in &KNIGHT_DIRS {
        if let Some(from) = to_sq(rank + dr, file + df)
            && matches!(board.piece_at(from),
                Some(Piece { piece_type: PieceType::Knight, color }) if color == by_color)
            {
                return true;
            }
    }

    // King attacks (needed to prevent the king from walking into an adjacent king)
    for &(dr, df) in &KING_DIRS {
        if let Some(from) = to_sq(rank + dr, file + df)
            && matches!(board.piece_at(from),
                Some(Piece { piece_type: PieceType::King, color }) if color == by_color)
            {
                return true;
            }
    }

    // Diagonal sliders (Bishop / Queen)
    for &(dr, df) in &BISHOP_DIRS {
        let (mut r, mut f) = (rank + dr, file + df);
        while (0..8).contains(&r) && (0..8).contains(&f) {
            if let Some(p) = board.piece_at((r * 8 + f) as Square) {
                if p.color == by_color
                    && (p.piece_type == PieceType::Bishop
                        || p.piece_type == PieceType::Queen)
                {
                    return true;
                }
                break;
            }
            r += dr;
            f += df;
        }
    }

    // Straight sliders (Rook / Queen)
    for &(dr, df) in &ROOK_DIRS {
        let (mut r, mut f) = (rank + dr, file + df);
        while (0..8).contains(&r) && (0..8).contains(&f) {
            if let Some(p) = board.piece_at((r * 8 + f) as Square) {
                if p.color == by_color
                    && (p.piece_type == PieceType::Rook || p.piece_type == PieceType::Queen)
                {
                    return true;
                }
                break;
            }
            r += dr;
            f += df;
        }
    }

    // Pawn attacks – a pawn of `by_color` attacks diagonally *forward* from its
    // own perspective, which is *backward* from the target square's perspective.
    let pawn_dir: i32 = match by_color {
        Color::White => -1, // White pawns stand below the target square.
        Color::Black => 1,
    };
    for &df in &[-1i32, 1i32] {
        if let Some(from) = to_sq(rank + pawn_dir, file + df)
            && matches!(board.piece_at(from),
                Some(Piece { piece_type: PieceType::Pawn, color }) if color == by_color)
            {
                return true;
            }
    }

    false
}

/// Returns `true` if the king of `color` is currently in check.
pub fn is_in_check(board: &Board, color: Color) -> bool {
    board
        .king_square(color)
        .map(|sq| is_square_attacked(board, sq, color.opposite()))
        .unwrap_or(false)
}

/// Applies `mv` to `board` and returns the resulting board state.
///
/// The caller must ensure `mv` was generated from `board`; applying an
/// arbitrary move to a mismatched board is undefined behaviour.
pub fn apply_move(board: &Board, mv: &Move) -> Board {
    let mut nb = board.clone();
    let piece = nb
        .take_piece(mv.from)
        .expect("apply_move: no piece at the from square");

    // Half-move clock: reset on pawn moves or captures.
    let is_capture =
        board.piece_at(mv.to).is_some() || mv.flag == MoveFlag::EnPassant;
    if is_capture || piece.piece_type == PieceType::Pawn {
        nb.halfmove_clock = 0;
    } else {
        nb.halfmove_clock += 1;
    }

    // En passant target square is valid only immediately after a double pawn push.
    nb.en_passant = if mv.flag == MoveFlag::DoublePawnPush {
        let ep_rank = if piece.color == Color::White {
            rank_of(mv.from) + 1
        } else {
            rank_of(mv.from) - 1
        };
        Some(make_square(file_of(mv.from), ep_rank))
    } else {
        None
    };

    // Handle move-specific side effects before placing the piece.
    match &mv.flag {
        MoveFlag::EnPassant => {
            // The captured pawn sits on the same rank as the capturing pawn
            // but on the file of the destination square.
            let captured_sq = make_square(file_of(mv.to), rank_of(mv.from));
            nb.take_piece(captured_sq);
        }

        MoveFlag::CastleKingside => {
            // Move the rook; the king will be placed below.
            let (rook_from, rook_to) = match piece.color {
                Color::White => (7u8, 5u8),  // h1 → f1
                Color::Black => (63u8, 61u8), // h8 → f8
            };
            let rook = nb.take_piece(rook_from);
            nb.set_piece(rook_to, rook);
        }

        MoveFlag::CastleQueenside => {
            let (rook_from, rook_to) = match piece.color {
                Color::White => (0u8, 3u8),  // a1 → d1
                Color::Black => (56u8, 59u8), // a8 → d8
            };
            let rook = nb.take_piece(rook_from);
            nb.set_piece(rook_to, rook);
        }

        MoveFlag::Promotion(promo_type) => {
            // Place the promoted piece (overwrites any captured piece at `to`).
            strip_castling_on_capture(&mut nb, mv.to);
            nb.set_piece(mv.to, Some(Piece::new(*promo_type, piece.color)));
            nb.side_to_move = piece.color.opposite();
            if piece.color == Color::Black {
                nb.fullmove_number += 1;
            }
            return nb; // Early return; castling-rights updates for pawn moves are no-ops.
        }

        MoveFlag::Normal | MoveFlag::DoublePawnPush => {
            strip_castling_on_capture(&mut nb, mv.to);
        }
    }

    // Place the moving piece at the destination.
    nb.set_piece(mv.to, Some(piece));

    // Update castling rights based on the piece that moved.
    strip_castling_on_move(&mut nb, mv.from, piece);

    nb.side_to_move = piece.color.opposite();
    if piece.color == Color::Black {
        nb.fullmove_number += 1;
    }

    nb
}

/// Revokes castling rights when a rook is captured on its home square.
fn strip_castling_on_capture(board: &mut Board, to: Square) {
    match to {
        0 => board.castling_rights.white_queenside = false,
        7 => board.castling_rights.white_kingside = false,
        56 => board.castling_rights.black_queenside = false,
        63 => board.castling_rights.black_kingside = false,
        _ => {}
    }
}

/// Revokes castling rights when the king or a rook moves away from its home square.
fn strip_castling_on_move(board: &mut Board, from: Square, piece: Piece) {
    match (piece.color, piece.piece_type) {
        (Color::White, PieceType::King) => {
            board.castling_rights.white_kingside = false;
            board.castling_rights.white_queenside = false;
        }
        (Color::Black, PieceType::King) => {
            board.castling_rights.black_kingside = false;
            board.castling_rights.black_queenside = false;
        }
        (Color::White, PieceType::Rook) => {
            if from == 0 {
                board.castling_rights.white_queenside = false;
            } else if from == 7 {
                board.castling_rights.white_kingside = false;
            }
        }
        (Color::Black, PieceType::Rook) => {
            if from == 56 {
                board.castling_rights.black_queenside = false;
            } else if from == 63 {
                board.castling_rights.black_kingside = false;
            }
        }
        _ => {}
    }
}

/// Generates all *legal* moves for the side to move.
///
/// A move is legal when it does not leave the mover's king in check and, for
/// castling, the king does not pass through or land on an attacked square.
pub fn generate_legal_moves(board: &Board) -> Vec<Move> {
    let color = board.side_to_move;
    generate_pseudo_legal_moves(board)
        .into_iter()
        .filter(|mv| {
            // Castling has additional constraints beyond the post-move check test.
            match mv.flag {
                MoveFlag::CastleKingside => {
                    if is_in_check(board, color) {
                        return false;
                    }
                    // King passes through mv.from + 1 (f1 / f8).
                    if is_square_attacked(board, mv.from + 1, color.opposite()) {
                        return false;
                    }
                }
                MoveFlag::CastleQueenside => {
                    if is_in_check(board, color) {
                        return false;
                    }
                    // King passes through mv.from - 1 (d1 / d8).
                    if is_square_attacked(board, mv.from - 1, color.opposite()) {
                        return false;
                    }
                }
                _ => {}
            }

            // The king must not be in check after the move is applied.
            let new_board = apply_move(board, mv);
            !is_in_check(&new_board, color)
        })
        .collect()
}