use std::fmt;
use crate::{alternating, Constraint, CountOp, Problem, SquareColor};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Piece {
King,
Queen,
Rook,
Bishop,
Knight,
}
impl fmt::Display for Piece {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::King => "K",
Self::Queen => "Q",
Self::Rook => "R",
Self::Bishop => "B",
Self::Knight => "N",
})
}
}
pub mod file {
pub const A: usize = 0;
pub const B: usize = 1;
pub const C: usize = 2;
pub const D: usize = 3;
pub const E: usize = 4;
pub const F: usize = 5;
pub const G: usize = 6;
pub const H: usize = 7;
#[must_use]
pub fn of(letter: char) -> Option<usize> {
match letter {
'a'..='h' => Some(letter as usize - 'a' as usize),
'A'..='H' => Some(letter as usize - 'A' as usize),
_ => None,
}
}
}
pub const STANDARD_BACK_RANK: [Piece; 8] = [
Piece::Rook,
Piece::Knight,
Piece::Bishop,
Piece::Queen,
Piece::King,
Piece::Bishop,
Piece::Knight,
Piece::Rook,
];
#[must_use]
pub fn back_rank_colors() -> Vec<SquareColor> {
alternating(8, SquareColor::Dark, SquareColor::Light)
}
fn alphabet() -> Vec<Piece> {
vec![
Piece::King,
Piece::Queen,
Piece::Rook,
Piece::Bishop,
Piece::Knight,
]
}
fn back_rank_counts() -> Vec<Constraint<Piece>> {
vec![
Constraint::Count {
piece: Piece::King,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Piece::Queen,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Piece::Rook,
op: CountOp::Eq,
value: 2,
},
Constraint::Count {
piece: Piece::Bishop,
op: CountOp::Eq,
value: 2,
},
Constraint::Count {
piece: Piece::Knight,
op: CountOp::Eq,
value: 2,
},
]
}
#[must_use]
pub fn standard() -> Problem<Piece> {
let mut constraints = back_rank_counts();
for (i, p) in STANDARD_BACK_RANK.iter().enumerate() {
constraints.push(Constraint::At {
piece: *p,
square: i,
});
}
Problem {
num_squares: 8,
square_colors: back_rank_colors(),
pieces: alphabet(),
constraint: Constraint::And(constraints),
}
}
#[must_use]
pub fn shuffle() -> Problem<Piece> {
Problem {
num_squares: 8,
square_colors: back_rank_colors(),
pieces: alphabet(),
constraint: Constraint::And(back_rank_counts()),
}
}
#[must_use]
pub fn chess_2880() -> Problem<Piece> {
let mut constraints = back_rank_counts();
constraints.push(Constraint::CountOnColor {
piece: Piece::Bishop,
color: SquareColor::Light,
op: CountOp::Eq,
value: 1,
});
constraints.push(Constraint::CountOnColor {
piece: Piece::Bishop,
color: SquareColor::Dark,
op: CountOp::Eq,
value: 1,
});
Problem {
num_squares: 8,
square_colors: back_rank_colors(),
pieces: alphabet(),
constraint: Constraint::And(constraints),
}
}
#[must_use]
pub fn chess_960() -> Chess960Problem {
let mut constraints = back_rank_counts();
constraints.push(Constraint::CountOnColor {
piece: Piece::Bishop,
color: SquareColor::Light,
op: CountOp::Eq,
value: 1,
});
constraints.push(Constraint::CountOnColor {
piece: Piece::Bishop,
color: SquareColor::Dark,
op: CountOp::Eq,
value: 1,
});
constraints.push(Constraint::Order(vec![
(Piece::Rook, 0),
(Piece::King, 0),
(Piece::Rook, 1),
]));
Chess960Problem {
inner: Problem {
num_squares: 8,
square_colors: back_rank_colors(),
pieces: alphabet(),
constraint: Constraint::And(constraints),
},
}
}
#[derive(Clone, Debug)]
pub struct Chess960Problem {
inner: Problem<Piece>,
}
const KNIGHT_PAIRS: [(usize, usize); 10] = [
(0, 1),
(0, 2),
(0, 3),
(0, 4),
(1, 2),
(1, 3),
(1, 4),
(2, 3),
(2, 4),
(3, 4),
];
const LIGHT_FILES: [usize; 4] = [1, 3, 5, 7];
const DARK_FILES: [usize; 4] = [0, 2, 4, 6];
impl Chess960Problem {
pub const COUNT: u32 = 960;
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
pub fn iter(&self) -> impl Iterator<Item = Vec<Piece>> + '_ {
self.inner.iter()
}
#[must_use]
pub fn at(&self, index: u64) -> Option<Vec<Piece>> {
self.inner.at(index)
}
#[must_use]
pub fn sample(&self, seed: u64) -> Vec<Piece> {
self.inner.sample(seed).expect("chess_960() is non-empty")
}
#[must_use]
pub fn sp_id(&self, id: u32) -> Option<Vec<Piece>> {
if id >= Self::COUNT {
return None;
}
let mut n = id;
let lb = (n % 4) as usize;
n /= 4;
let db = (n % 4) as usize;
n /= 4;
let q_idx = (n % 6) as usize;
n /= 6;
let kn_idx = n as usize;
let mut board: [Option<Piece>; 8] = [None; 8];
board[LIGHT_FILES[lb]] = Some(Piece::Bishop);
board[DARK_FILES[db]] = Some(Piece::Bishop);
let empty_after_bishops: Vec<usize> = (0..8).filter(|i| board[*i].is_none()).collect();
board[empty_after_bishops[q_idx]] = Some(Piece::Queen);
let empty_after_queen: Vec<usize> = (0..8).filter(|i| board[*i].is_none()).collect();
let (kn_a, kn_b) = KNIGHT_PAIRS[kn_idx];
board[empty_after_queen[kn_a]] = Some(Piece::Knight);
board[empty_after_queen[kn_b]] = Some(Piece::Knight);
let empty_last: Vec<usize> = (0..8).filter(|i| board[*i].is_none()).collect();
board[empty_last[0]] = Some(Piece::Rook);
board[empty_last[1]] = Some(Piece::King);
board[empty_last[2]] = Some(Piece::Rook);
Some(board.iter().map(|p| p.expect("filled")).collect())
}
#[must_use]
pub fn sp_id_of(&self, arrangement: &[Piece]) -> Option<u32> {
if arrangement.len() != 8 {
return None;
}
let bishops: Vec<usize> = arrangement
.iter()
.enumerate()
.filter_map(|(i, p)| (*p == Piece::Bishop).then_some(i))
.collect();
if bishops.len() != 2 {
return None;
}
let (light_sq, dark_sq) = if bishops[0] % 2 == 1 {
(bishops[0], bishops[1])
} else {
(bishops[1], bishops[0])
};
if light_sq % 2 != 1 || dark_sq % 2 != 0 {
return None;
}
let lb = LIGHT_FILES.iter().position(|&x| x == light_sq)?;
let db = DARK_FILES.iter().position(|&x| x == dark_sq)?;
let queen = arrangement.iter().position(|p| *p == Piece::Queen)?;
if arrangement.iter().filter(|p| **p == Piece::Queen).count() != 1 {
return None;
}
let empty_after_bishops: Vec<usize> = (0..8)
.filter(|i| arrangement[*i] != Piece::Bishop)
.collect();
let q_idx = empty_after_bishops.iter().position(|&x| x == queen)?;
let knight_positions: Vec<usize> = arrangement
.iter()
.enumerate()
.filter_map(|(i, p)| (*p == Piece::Knight).then_some(i))
.collect();
if knight_positions.len() != 2 {
return None;
}
let empty_after_queen: Vec<usize> = (0..8)
.filter(|i| arrangement[*i] != Piece::Bishop && arrangement[*i] != Piece::Queen)
.collect();
let kn_a = empty_after_queen
.iter()
.position(|&x| x == knight_positions[0])?;
let kn_b = empty_after_queen
.iter()
.position(|&x| x == knight_positions[1])?;
let kn_idx = KNIGHT_PAIRS.iter().position(|&p| p == (kn_a, kn_b))?;
let last_three: Vec<usize> = (0..8)
.filter(|i| {
arrangement[*i] != Piece::Bishop
&& arrangement[*i] != Piece::Queen
&& arrangement[*i] != Piece::Knight
})
.collect();
if last_three.len() != 3 {
return None;
}
if arrangement[last_three[0]] != Piece::Rook
|| arrangement[last_three[1]] != Piece::King
|| arrangement[last_three[2]] != Piece::Rook
{
return None;
}
let id = ((kn_idx as u32 * 6 + q_idx as u32) * 4 + db as u32) * 4 + lb as u32;
Some(id)
}
#[must_use]
pub fn with_constraint(&self, c: Constraint<Piece>) -> Problem<Piece> {
self.inner.with_constraint(c)
}
#[must_use]
pub fn into_problem(self) -> Problem<Piece> {
self.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_count_is_one() {
assert_eq!(standard().count(), 1);
}
#[test]
fn shuffle_count_is_5040() {
assert_eq!(shuffle().count(), 5040);
}
#[test]
fn chess_2880_count_is_2880() {
assert_eq!(chess_2880().count(), 2880);
}
#[test]
fn chess_960_count_is_960() {
assert_eq!(chess_960().count(), 960);
}
#[test]
fn standard_arrangement_matches_fide() {
let arrangements: Vec<Vec<Piece>> = standard().iter().collect();
assert_eq!(arrangements.len(), 1);
assert_eq!(arrangements[0], STANDARD_BACK_RANK.to_vec());
}
#[test]
fn chess_960_minus_king_constraint_equals_chess_2880() {
assert_eq!(chess_960().count() * 3, chess_2880().count());
}
#[test]
fn with_constraint_narrows_chess_960() {
let narrowed = chess_960().with_constraint(Constraint::At {
piece: Piece::Queen,
square: 3,
});
assert!(narrowed.count() < 960);
assert!(narrowed.count() > 0);
}
#[test]
fn file_constants_match_alphabet() {
use file::*;
assert_eq!([A, B, C, D, E, F, G, H], [0, 1, 2, 3, 4, 5, 6, 7]);
}
#[test]
fn file_letter_to_index() {
for (i, ch) in ('a'..='h').enumerate() {
assert_eq!(file::of(ch), Some(i));
assert_eq!(file::of(ch.to_ascii_uppercase()), Some(i));
}
assert_eq!(file::of('i'), None);
assert_eq!(file::of('1'), None);
assert_eq!(file::of(' '), None);
}
#[test]
fn file_constants_usable_in_at_constraint() {
let with_queen_on_d = chess_960().with_constraint(Constraint::At {
piece: Piece::Queen,
square: file::D,
});
assert!(with_queen_on_d.count() > 0);
assert!(with_queen_on_d.count() < 960);
}
#[test]
fn sp_id_518_is_standard_position() {
let preset = chess_960();
assert_eq!(preset.sp_id(518), Some(STANDARD_BACK_RANK.to_vec()));
assert_eq!(preset.sp_id_of(&STANDARD_BACK_RANK), Some(518));
}
#[test]
fn sp_id_out_of_range_returns_none() {
let preset = chess_960();
assert_eq!(preset.sp_id(960), None);
assert_eq!(preset.sp_id(u32::MAX), None);
}
#[test]
fn sp_id_roundtrip_over_full_range() {
let preset = chess_960();
for id in 0..Chess960Problem::COUNT {
let arrangement = preset.sp_id(id).expect("in range");
assert_eq!(arrangement.len(), 8);
assert_eq!(preset.sp_id_of(&arrangement), Some(id), "round-trip {id}");
}
}
#[test]
fn sample_is_deterministic_in_seed() {
let preset = chess_960();
let a = preset.sample(0xC0FFEE);
let b = preset.sample(0xC0FFEE);
assert_eq!(a, b);
assert!(preset.sp_id_of(&a).is_some());
}
#[test]
fn sp_id_of_rejects_invalid_arrangements() {
let preset = chess_960();
assert_eq!(preset.sp_id_of(&[Piece::King; 4]), None);
let mut bad = STANDARD_BACK_RANK.to_vec();
bad[1] = Piece::Bishop;
bad[2] = Piece::Knight;
assert_eq!(preset.sp_id_of(&bad), None);
let king_outside = vec![
Piece::King,
Piece::Rook,
Piece::Rook,
Piece::Bishop,
Piece::Knight,
Piece::Knight,
Piece::Bishop,
Piece::Queen,
];
assert_eq!(preset.sp_id_of(&king_outside), None);
let two_queens = vec![
Piece::Rook,
Piece::Knight,
Piece::Bishop,
Piece::Queen,
Piece::Queen,
Piece::Bishop,
Piece::Knight,
Piece::Rook,
];
assert_eq!(preset.sp_id_of(&two_queens), None);
}
}