bitstackchess 0.1.1

A bitboard‐based chess game engine with 10 × u128 move history
Documentation
// src/cli_adapter.rs

//! A simple command‐line interface for our ChessEngine (GameCore).
//! 
//! • Reads moves in “long algebraic” format, e.g. “e2e4” (source→dest).
//! • Verifies that the piece at `src` belongs to the side to move.
//! • Converts that to Move10 (using pid % 16).
//! • Verifies it’s in `generate_pseudo_legal_moves()`, then calls `push_move(...)`.
//! • Supports “undo” to pop the last move.
//! • Prints the board (ASCII) along with turn count, side to move, and “Check!” notification.
//! • On checkmate or stalemate, displays a “game over” screen with the result.

use std::io::{self, Write};

use crate::engine::{GameCore, ChessEngine};
use crate::core::Move10;
use crate::board::PieceMapping;
use crate::rules::checkmate::{CheckmateLogic, Color, GameResult};

/// Map each PID (0..31) to a single‐character “piece symbol” for ASCII drawing:
///   • White:  P(awn)=p, R(ook)=r, N= n, B= b, Q= q, K= k  
///   • Black:  p → uppercase, etc.  
fn pid_to_char(pid: u8) -> char {
    match pid {
        0..=7 => 'p',   // White pawn
        8 | 9 => 'r',   // White rook
        10 | 11 => 'n', // White knight
        12 | 13 => 'b', // White bishop
        14 => 'q',      // White queen
        15 => 'k',      // White king
        16..=23 => 'P', // Black pawn
        24 | 25 => 'R', // Black rook
        26 | 27 => 'N', // Black knight
        28 | 29 => 'B', // Black bishop
        30 => 'Q',      // Black queen
        31 => 'K',      // Black king
        _ => '.',       // Should not happen
    }
}

/// Print the current board as ASCII, preceded by turn count and side to move.
/// Also prints “Check!” if the side to move is in check.
/// Ranks 8→1, files a→h.
/// Example:
///   Turn 1: White to move   Check!
///   8 | r n b q k b n r
///   7 | p p p p p p p p
///    ...
///   1 | R N B Q K B N R
///      ----------------
///       a b c d e f g h
fn print_board_with_header(mapping: &PieceMapping, occupied: u64, ply: usize, side: Color) {
    // Compute turn number as (ply + 1)
    let turn = ply + 1;
    let side_str = match side {
        Color::White => "White",
        Color::Black => "Black",
    };
    // Determine if side_to_move is in check
    let king_pid = if side == Color::White { 15 } else { 31 };
    let in_check = CheckmateLogic::is_in_check(mapping, occupied, king_pid);
    if in_check {
        println!("\nTurn {}: {} to move   Check!", turn, side_str);
    } else {
        println!("\nTurn {}: {} to move", turn, side_str);
    }

    // Build a 2D array [rank][file] of characters
    let mut board: [[char; 8]; 8] = [['.'; 8]; 8];
    for pid in 0..32 {
        if let Some(sq) = mapping.piece_square[pid as usize] {
            let rank = (sq >> 3) as usize;
            let file = (sq & 7) as usize;
            board[rank][file] = pid_to_char(pid as u8);
        }
    }

    for rank in (0..8).rev() {
        print!("{} |", rank + 1);
        for file in 0..8 {
            print!(" {}", board[rank][file]);
        }
        println!();
    }
    println!("   ----------------");
    println!("    a b c d e f g h\n");
}

/// Parse a move string in “long algebraic” format “e2e4” (exactly 4 characters:
/// source‐file (a–h), source‐rank (1–8), dest‐file (a–h), dest‐rank (1–8)).
/// Returns `Some((src_sq, dst_sq))` in 0..63, or `None` if invalid.
fn parse_long_algebraic(input: &str) -> Option<(u8, u8)> {
    let bytes = input.trim().as_bytes();
    if bytes.len() != 4 {
        return None;
    }
    let sf = bytes[0] as char;
    let sr = bytes[1] as char;
    let df = bytes[2] as char;
    let dr = bytes[3] as char;

    // file a→0, b→1, ..., h→7
    let file_from = (sf as u8).wrapping_sub(b'a');
    let rank_from = (sr as u8).wrapping_sub(b'1');
    let file_to = (df as u8).wrapping_sub(b'a');
    let rank_to = (dr as u8).wrapping_sub(b'1');

    if file_from < 8 && rank_from < 8 && file_to < 8 && rank_to < 8 {
        let src_sq = (rank_from << 3) | file_from;
        let dst_sq = (rank_to << 3) | file_to;
        Some((src_sq, dst_sq))
    } else {
        None
    }
}

/// The main CLI loop. Creates a `GameCore`, displays the empty board with header, then repeatedly:
///   • Prompts “Enter move (e.g. e2e4), ‘undo’, or ‘quit’:”
///   • Reads a line. If “quit”, exit. If “undo”, pop_move and reprint board. Else parse move:
///     1) Check that `mapping.who_on_square(src)` is Some and matches side to move.
///     2) Build a `Move10` using pid % 16.
///     3) Verify it’s in `generate_pseudo_legal_moves()`.
///     4) Attempt `push_move`, then reprint board with updated header or print error.
///   • On checkmate or stalemate, displays a “game over” screen with the result.
pub fn run_cli() {
    let mut engine: GameCore = GameCore::default();

    println!("Welcome to Chess CLI!");
    println!("Enter moves in ‘long algebraic’ (e.g. e2e4), ‘undo’, or ‘quit’.");
    print_board_with_header(
        engine.current_mapping(),
        engine.current_occupied(),
        engine.ply(),
        engine.side_to_move(),
    );

    loop {
        print!("Move> ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        if io::stdin().read_line(&mut input).is_err() {
            println!("Error reading input, exiting.");
            break;
        }
        let trimmed = input.trim();
        if trimmed.eq_ignore_ascii_case("quit") || trimmed.eq_ignore_ascii_case("exit") {
            println!("Goodbye!");
            break;
        }
        if trimmed.eq_ignore_ascii_case("undo") {
            if engine.ply() == 0 {
                println!("No moves to undo.");
            } else {
                engine.pop_move();
                print_board_with_header(
                    engine.current_mapping(),
                    engine.current_occupied(),
                    engine.ply(),
                    engine.side_to_move(),
                );
            }
            continue;
        }
        if trimmed.is_empty() {
            continue;
        }

        // 1) Parse “e2e4”
        match parse_long_algebraic(trimmed) {
            Some((src, dst)) => {
                let mapping = engine.current_mapping();
                // 2) Ensure a piece is on `src`:
                if let Some(actual_pid) = mapping.who_on_square(src) {
                    // 3) Ensure it belongs to side_to_move:
                    let side = engine.side_to_move();
                    let piece_color = if actual_pid < 16 {
                        Color::White
                    } else {
                        Color::Black
                    };
                    if piece_color != side {
                        println!("Illegal: trying to move opponent’s piece.");
                        continue;
                    }

                    // 4) Build Move10 with pid_within = actual_pid % 16:
                    let pid_within = actual_pid % 16;
                    let mv10 = Move10::new(pid_within, dst);

                    // 5) Verify it’s pseudo‐legal:
                    let legal_moves = engine.generate_pseudo_legal_moves();
                    if !legal_moves.contains(&mv10) {
                        println!("Move not allowed by piece rules (not pseudo-legal).");
                        continue;
                    }

                    // 6) Attempt push_move (which also checks for king-in-check):
                    match engine.push_move(mv10) {
                        Ok(()) => {
                            // Check for game over immediately after applying the move:
                            let result = engine.game_result();
                            match result {
                                GameResult::Ongoing => {
                                    print_board_with_header(
                                        engine.current_mapping(),
                                        engine.current_occupied(),
                                        engine.ply(),
                                        engine.side_to_move(),
                                    );
                                }
                                GameResult::Stalemate => {
                                    print_board_with_header(
                                        engine.current_mapping(),
                                        engine.current_occupied(),
                                        engine.ply(),
                                        engine.side_to_move(),
                                    );
                                    println!("\nGame Over: Stalemate. It's a draw.");
                                    break;
                                }
                                GameResult::Checkmate(winner) => {
                                    print_board_with_header(
                                        engine.current_mapping(),
                                        engine.current_occupied(),
                                        engine.ply(),
                                        engine.side_to_move(), // next side, but game over
                                    );
                                    let winner_str = match winner {
                                        Color::White => "White",
                                        Color::Black => "Black",
                                    };
                                    println!("\nGame Over: {} wins by checkmate!", winner_str);
                                    break;
                                }
                            }
                        }
                        Err(e) => {
                            println!("Illegal move (leaves king in check): {:?}", e);
                        }
                    }
                } else {
                    println!("No piece on source square.");
                }
            }
            None => {
                println!("Invalid format. Use e.g. ‘e2e4’.");
            }
        }
    }
}