use std::{collections::BTreeMap, ffi::OsStr, fmt::Write, num::NonZeroU32};
use console::style;
use prettytable::{Cell, Row, Table};
use shakmaty::{
fen::Epd, san::San, uci::Uci, Bitboard, Board, CastlingMode, CastlingSide, Chess, Color,
EnPassantMode, File, FromSetup, Piece, Position, PositionError, Rank, Role, Setup, Square,
};
use termtree::Tree;
use crate::{built_info, tr};
pub const ROOT: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -";
#[derive(Clone, Debug)]
pub struct ChessMove(pub Uci);
impl clap::builder::ValueParserFactory for ChessMove {
type Parser = ChessMoveValueParser;
fn value_parser() -> Self::Parser {
ChessMoveValueParser
}
}
#[derive(Clone, Debug)]
pub struct ChessMoveValueParser;
impl clap::builder::TypedValueParser for ChessMoveValueParser {
type Value = ChessMove;
fn parse_ref(
&self,
cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &OsStr,
) -> Result<Self::Value, clap::Error> {
let value_str = value.to_str().ok_or_else(|| {
let mut cmd = cmd.clone();
cmd.error(
clap::error::ErrorKind::InvalidValue,
tr!("Unable to parse UCI move"),
)
})?;
let uci = Uci::from_ascii(value_str.as_bytes()).map_err(|_| {
let mut cmd = cmd.clone();
cmd.error(
clap::error::ErrorKind::InvalidValue,
tr!("Invalid UCI move"),
)
})?;
Ok(ChessMove(uci))
}
}
pub fn serialize_chess(chess: &Chess) -> [u64; 5] {
let mut arr = [0u64; 5];
static PIECE_VALUES: [[u64; 7]; 2] = [
[0, 1, 3, 5, 7, 9, 11], [0, 2, 4, 6, 8, 10, 12], ];
let board = chess.board();
for square in board.occupied() {
let piece = unsafe { board.piece_at(square).unwrap_unchecked() };
let piece_val = PIECE_VALUES[piece.color as usize][piece.role as usize];
let arr_index = u8::from(square) / 16;
let bit_offset = (u8::from(square) % 16) * 4;
arr[arr_index as usize] |= piece_val << bit_offset;
}
let turn_bit = u64::from(chess.turn() == Color::Black);
let castles = chess.castles();
let castling_bits = if castles.has(Color::White, CastlingSide::KingSide) {
1 << 1
} else {
0
} | if castles.has(Color::White, CastlingSide::QueenSide) {
1 << 2
} else {
0
} | if castles.has(Color::Black, CastlingSide::KingSide) {
1 << 3
} else {
0
} | if castles.has(Color::Black, CastlingSide::QueenSide) {
1 << 4
} else {
0
};
let ep_square = chess.ep_square(EnPassantMode::Legal);
let ep_bits = ep_square.map_or(0, |sq| (sq.file().char() as u64 - 'a' as u64 + 1) << 5);
arr[4] = turn_bit | castling_bits | ep_bits;
arr
}
pub fn deserialize_chess(arr: [u64; 5]) -> Result<Chess, PositionError<Chess>> {
let mut board = Board::empty();
for rank in 0..8 {
for file in 0..8 {
let square_index = rank * 8 + file;
let square = unsafe { Square::new_unchecked(square_index) };
let arr_index = square_index / 16;
let bit_offset = (square_index % 16) * 4;
let piece_val = ((arr[arr_index as usize] >> bit_offset) & 0xF) as u8;
let piece = match piece_val {
0 => {
continue;
}
1 => Piece {
color: Color::Black,
role: Role::Pawn,
},
2 => Piece {
color: Color::White,
role: Role::Pawn,
},
3 => Piece {
color: Color::Black,
role: Role::Knight,
},
4 => Piece {
color: Color::White,
role: Role::Knight,
},
5 => Piece {
color: Color::Black,
role: Role::Bishop,
},
6 => Piece {
color: Color::White,
role: Role::Bishop,
},
7 => Piece {
color: Color::Black,
role: Role::Rook,
},
8 => Piece {
color: Color::White,
role: Role::Rook,
},
9 => Piece {
color: Color::Black,
role: Role::Queen,
},
10 => Piece {
color: Color::White,
role: Role::Queen,
},
11 => Piece {
color: Color::Black,
role: Role::King,
},
12 => Piece {
color: Color::White,
role: Role::King,
},
n => {
panic!(
"{}",
tr!(
"Invalid piece index {} while deserializing {}. Please report a bug!",
n,
format!("{:?}", arr)
)
);
}
};
board.set_piece_at(square, piece);
}
}
let turn_bit = arr[4] & 1;
let turn = if turn_bit == 1 {
Color::Black
} else {
Color::White
};
let castling_bits = (arr[4] >> 1) & 0xF;
let wrooks: Bitboard = board
.by_piece(Piece {
color: Color::White,
role: Role::Rook,
})
.into_iter()
.filter(|&square| square.rank() == Rank::First)
.collect();
let brooks: Bitboard = board
.by_piece(Piece {
color: Color::Black,
role: Role::Rook,
})
.into_iter()
.filter(|&square| square.rank() == Rank::Eighth)
.collect();
let mut castling_rights = Bitboard::EMPTY;
if (castling_bits & 1) != 0 {
castling_rights |= wrooks
.into_iter()
.max_by_key(|&square| square.file())
.expect("missing white kingside rook");
}
if (castling_bits & 2) != 0 {
castling_rights |= wrooks
.into_iter()
.min_by_key(|&square| square.file())
.expect("missing white queenside rook");
}
if (castling_bits & 4) != 0 {
castling_rights |= brooks
.into_iter()
.max_by_key(|&square| square.file())
.expect("missing black kingside rook");
}
if (castling_bits & 8) != 0 {
castling_rights |= brooks
.into_iter()
.min_by_key(|&square| square.file())
.expect("missing black queenside rook");
}
let ep_file = ((arr[4] >> 5) & 0xF) as u32;
let ep_rank = match turn {
Color::Black => Rank::Third,
Color::White => Rank::Sixth,
};
let ep_square = if ep_file > 0 && ep_file <= 8 {
Some(Square::from_coords(File::new(ep_file - 1), ep_rank))
} else {
None
};
let setup = Setup {
board,
promoted: Bitboard::EMPTY, pockets: None, turn,
castling_rights,
ep_square,
remaining_checks: None, halfmoves: 0, fullmoves: NonZeroU32::new(1).unwrap(), };
Chess::from_setup(setup, CastlingMode::Chess960)
}
pub fn lines_from_tree(tree: &Tree<String>, max_ply: u16) -> Table {
let mut table = Table::new();
table.set_titles(Row::new(vec![Cell::new(&tree.root)]));
let is_white_to_move = tree.root.contains(" w ");
fn traverse_tree(
tree: &Tree<String>,
table: &mut Table,
path: &mut Vec<String>,
max_ply: u16,
is_white_to_move: bool,
) {
if path.len() == max_ply as usize * 2 || tree.leaves.is_empty() {
if !path.is_empty() {
let mut opening = String::with_capacity(256);
let offset = if is_white_to_move { 0 } else { 1 };
if !is_white_to_move {
opening.push_str("1. ... ");
}
for (i, move_str) in path.iter().enumerate() {
if (i + offset) % 2 == 0 {
write!(&mut opening, "{}. ", (i + offset) / 2 + 1).unwrap();
}
write!(&mut opening, "{} ", move_str).unwrap();
}
table.add_row(Row::new(vec![Cell::new(opening.trim())]));
}
} else {
for child in &tree.leaves {
path.push(child.root.clone());
traverse_tree(child, table, path, max_ply, is_white_to_move);
path.pop();
}
}
}
traverse_tree(tree, &mut table, &mut vec![], max_ply, is_white_to_move);
table
}
#[allow(clippy::too_many_arguments)]
#[deprecated(
since = "0.7.0",
note = "Please use the `write_pgn` function of the respective opening book implementation instead"
)]
pub fn pgn_from_tree(
tree: &Tree<String>,
output: &mut Box<dyn std::io::Write + Send>,
event: &str,
site: &str,
date: &str,
white: &str,
black: &str,
result: &str,
max_ply: u16,
) -> Result<(), Box<dyn std::error::Error>> {
let mut round = 1;
let root = tree.root.as_str();
let black_to_move = root.contains(" b ");
#[allow(clippy::too_many_arguments)]
fn traverse_tree(
root: &str,
tree: &Tree<String>,
output: &mut Box<dyn std::io::Write + Send>,
path: &mut Vec<String>,
max_ply: u16,
round: &mut usize,
event: &str,
site: &str,
date: &str,
white: &str,
black: &str,
result: &str,
black_to_move: bool,
) {
if path.len() == max_ply as usize * 2 || tree.leaves.is_empty() {
if !path.is_empty() {
let opening = path
.iter()
.enumerate()
.map(|(i, move_text)| {
let move_number = (i + (if black_to_move { 1 } else { 0 })) / 2 + 1;
if i == 0 || i % 2 == if black_to_move { 1 } else { 0 } {
format!(
"{}.{} {}",
move_number,
if black_to_move && i == 0 { " ..." } else { "" },
move_text
)
} else {
move_text.to_string()
}
})
.collect::<Vec<String>>()
.join(" ");
writeln!(
output,
"[Event \"{}\"]\n\
[Site \"{}\"]\n\
[Date \"{}\"]\n\
[Round \"{}\"]\n\
[White \"{}\"]\n\
[Black \"{}\"]\n\
[Result \"{}\"]\n\
[Annotator \"{} v{}\"]",
event,
site,
date,
round,
white,
black,
result,
built_info::PKG_NAME,
built_info::PKG_VERSION,
)
.unwrap();
if root != ROOT {
writeln!(output, "[FEN \"{} 0 1\"]", root).unwrap();
writeln!(output, "[SetUp \"1\"]").unwrap();
}
write!(output, "\n{} {}\n\n", opening, result).unwrap();
*round += 1;
}
} else {
for child in &tree.leaves {
path.push(child.root.clone());
traverse_tree(
root,
child,
output,
path,
max_ply,
round,
event,
site,
date,
white,
black,
result,
black_to_move,
);
path.pop();
}
}
}
traverse_tree(
root,
tree,
output,
&mut vec![],
max_ply,
&mut round,
event,
site,
date,
white,
black,
result,
black_to_move,
);
Ok(())
}
pub fn edit_comment(position: Chess, key: u64) -> String {
let epd = format!(
"{}",
Epd::from_position(position.clone(), EnPassantMode::PseudoLegal)
);
let mut moves = BTreeMap::new();
for move_ in position.legal_moves() {
let san = San::from_move(&position, &move_);
let uci = move_.to_uci(CastlingMode::Standard);
moves.insert(format!("{}", san), format!("{}", uci));
}
let key_prefix = tr!("# key: ");
let epd_prefix = tr!("# epd: ");
let mut san_line = tr!("# san: ").to_string();
let mut uci_line = tr!("# uci: ").to_string();
for (idx, (san, uci)) in moves.iter().enumerate() {
if idx != 0 {
san_line.push_str(", ");
uci_line.push_str(", ");
}
san_line.push_str(san);
uci_line.push_str(uci);
}
format!(
"{}{:#x}\n{}{}\n{}\n{}",
key_prefix, key, epd_prefix, epd, san_line, uci_line
)
}
pub fn square_mirror_file(s: Square) -> Square {
unsafe { Square::new_unchecked(u32::from(s as u8 ^ 0x07)) }
}
pub fn king_file(position: &dyn Position) -> File {
position.board().king_of(position.turn()).unwrap().file()
}
pub fn has_en_passant(position: &dyn Position) -> bool {
!position.en_passant_moves().is_empty()
}
pub fn get_board_lines(position: &Chess, color: bool) -> Vec<String> {
let board: &Board = position.board();
let mut board_lines = Vec::new();
for row in 0..8 {
let mut line = format!(" {} ", 8 - row);
for col in 0..8 {
let idx = 64 - (row + 1) * 8 + col;
let square = Square::new(idx);
let piece = board.piece_at(square);
let square_is_white = (row + col) % 2 == 0;
let c = if square_is_white { 140 } else { 80 };
let mut piece_char = piece
.map(|p: Piece| {
let mut ch = piece_ascii(&p);
if color {
ch = if p.color == Color::White {
style(ch).blue().to_string()
} else {
style(ch).red().to_string()
};
}
ch
})
.unwrap_or("·".to_string());
if color {
piece_char = style(piece_char).on_color256(c).to_string();
}
let formatted_piece_char = format!("{} ", piece_char);
line.push_str(&formatted_piece_char);
}
board_lines.push(line);
}
let bottom_line = format!(
" {}",
(b'a'..=b'h')
.map(char::from)
.map(|c| c.to_string())
.collect::<Vec<String>>()
.join(" ")
);
board_lines.push(bottom_line);
board_lines
}
fn piece_ascii(piece: &Piece) -> String {
match (piece.role, piece.color) {
(shakmaty::Role::Pawn, shakmaty::Color::Black) => "♟︎",
(shakmaty::Role::Pawn, shakmaty::Color::White) => "♟︎",
(shakmaty::Role::Knight, shakmaty::Color::Black) => "♞",
(shakmaty::Role::Knight, shakmaty::Color::White) => "♞",
(shakmaty::Role::Bishop, shakmaty::Color::Black) => "♝",
(shakmaty::Role::Bishop, shakmaty::Color::White) => "♝",
(shakmaty::Role::Rook, shakmaty::Color::Black) => "♜",
(shakmaty::Role::Rook, shakmaty::Color::White) => "♜",
(shakmaty::Role::Queen, shakmaty::Color::Black) => "♛",
(shakmaty::Role::Queen, shakmaty::Color::White) => "♛",
(shakmaty::Role::King, shakmaty::Color::Black) => "♚",
(shakmaty::Role::King, shakmaty::Color::White) => "♚",
}
.to_string()
}
#[cfg(test)]
mod tests {
use shakmaty::{fen::Epd, CastlingMode, Chess, File};
use super::*;
#[test]
fn test_king_file_white() {
let epd =
Epd::from_ascii(b"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
let pos: Chess = epd.into_position(CastlingMode::Standard).unwrap();
let white_king_file = king_file(&pos);
assert_eq!(white_king_file, File::E);
}
#[test]
fn test_king_file_black() {
let epd =
Epd::from_ascii(b"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
let pos: Chess = epd.into_position(CastlingMode::Standard).unwrap();
let black_king_file = king_file(&pos);
assert_eq!(black_king_file, File::E);
}
#[test]
fn test_king_file_custom_position() {
let epd = Epd::from_ascii(
b"r1bqr1k1/1pp2ppp/p1np1n2/2b1p1B1/2B1P3/2NP4/PPPQNPPP/2KR3R w - - 0 9",
)
.unwrap();
let pos: Chess = epd.into_position(CastlingMode::Standard).unwrap();
let white_king_file = king_file(&pos);
assert_eq!(white_king_file, File::C);
let pos = pos.swap_turn().unwrap();
let black_king_file = king_file(&pos);
assert_eq!(black_king_file, File::G);
}
#[test]
fn test_has_en_passant_true() {
let epd =
Epd::from_ascii(b"rnbqkbnr/ppp3pp/4p3/3pPp2/3P4/8/PPP2PPP/RNBQKBNR w KQkq f6 0 4")
.unwrap();
let pos: Chess = epd.into_position(CastlingMode::Standard).unwrap();
assert_eq!(has_en_passant(&pos), true);
}
#[test]
fn test_has_en_passant_false() {
let epd =
Epd::from_ascii(b"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
let pos: Chess = epd.into_position(CastlingMode::Standard).unwrap();
assert_eq!(has_en_passant(&pos), false);
}
#[test]
fn test_chess_serialize_root() {
let epd =
Epd::from_ascii(b"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
let pos: Chess = epd.clone().into_position(CastlingMode::Standard).unwrap();
let vec = serialize_chess(&pos);
let pos_new = deserialize_chess(vec).unwrap();
let epd_new = Epd::from_position(pos_new, EnPassantMode::Legal);
assert_eq!(epd, epd_new);
}
#[test]
fn test_chess_serialize_castling() {
let epd =
Epd::from_ascii(b"r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 0 1")
.unwrap();
let pos: Chess = epd.clone().into_position(CastlingMode::Standard).unwrap();
let vec = serialize_chess(&pos);
let pos_new = deserialize_chess(vec).unwrap();
let epd_new = Epd::from_position(pos_new, EnPassantMode::Legal);
assert_eq!(epd, epd_new);
}
#[test]
fn test_chess_serialize_en_passant() {
let epd =
Epd::from_ascii(b"rnbqkbnr/ppp3pp/4p3/3pPp2/3P4/8/PPP2PPP/RNBQKBNR w KQkq f6 0 1")
.unwrap();
let pos: Chess = epd.clone().into_position(CastlingMode::Standard).unwrap();
let vec = serialize_chess(&pos);
let pos_new = deserialize_chess(vec).unwrap();
let epd_new = Epd::from_position(pos_new, EnPassantMode::Legal);
assert_eq!(epd, epd_new);
}
#[test]
fn test_chess_serialize_ignore_chess960_castling() {
let epd =
Epd::from_ascii(b"nrbknbrq/pppppppp/8/8/8/8/PPPPPPPP/NRBKNBRQ w KQkq - 0 1").unwrap();
let pos: Chess = epd.clone().into_position(CastlingMode::Chess960).unwrap();
let vec = serialize_chess(&pos);
let pos_new = deserialize_chess(vec).unwrap();
let epd_new = Epd::from_position(pos_new, EnPassantMode::Legal);
assert_eq!(epd, epd_new);
}
#[test]
fn test_chess_serialize_castling_with_other_rook_on_non_back_rank() {
let epd = Epd::from_ascii(b"r2qkb2/ppp2pp1/2n1p1b1/3P4/7r/1PB2B2/P1P1Q3/2KR3R w q - 0 1")
.unwrap();
let pos: Chess = epd.clone().into_position(CastlingMode::Chess960).unwrap();
let vec = serialize_chess(&pos);
let pos_new = deserialize_chess(vec).unwrap();
let epd_new = Epd::from_position(pos_new, EnPassantMode::Legal);
assert_eq!(epd, epd_new);
}
}