use crate::domain::piece::Piece;
use crate::domain::side::Side;
use crate::domain::square::Square;
use crate::domain::variant::Variant;
use crate::movement::{foot_soldier, gold_directions, leaper, slider, stepper, Occupancy};
#[must_use]
pub fn is_attacked(
target: Square,
by: Side,
by_variant: Variant,
piece_at: impl Fn(Square) -> Option<Piece>,
) -> bool {
let occupancy = |s: Square| Occupancy::of(piece_at(s).map(Piece::side), by);
Square::all().any(|from| match piece_at(from) {
Some(piece) if piece.belongs_to(by) => {
piece_attacks(by_variant, piece, by, from, target, &occupancy)
}
_ => false,
})
}
#[must_use]
pub fn attackers_of(
target: Square,
by: Side,
by_variant: Variant,
piece_at: impl Fn(Square) -> Option<Piece>,
) -> Vec<Square> {
let occupancy = |s: Square| Occupancy::of(piece_at(s).map(Piece::side), by);
Square::all()
.filter(|&from| match piece_at(from) {
Some(piece) if piece.belongs_to(by) => {
piece_attacks(by_variant, piece, by, from, target, &occupancy)
}
_ => false,
})
.collect()
}
#[must_use]
pub fn attacks_from(
from: Square,
from_variant: Variant,
target: Square,
piece_at: impl Fn(Square) -> Option<Piece>,
) -> bool {
match piece_at(from) {
Some(piece) => {
let side = piece.side();
let occupancy = |s: Square| Occupancy::of(piece_at(s).map(Piece::side), side);
piece_attacks(from_variant, piece, side, from, target, &occupancy)
}
None => false,
}
}
fn piece_attacks(
variant: Variant,
piece: Piece,
side: Side,
from: Square,
target: Square,
occupancy: &impl Fn(Square) -> Occupancy,
) -> bool {
match piece.kind_letter() {
'K' => stepper::attacks(from, &stepper::KING, target),
'G' => slider::attacks(from, &slider::ORTHOGONAL, target, occupancy),
'Q' => slider::attacks(from, &slider::OCTILINEAR, target, occupancy),
'R' => slider::attacks(from, &slider::ORTHOGONAL, target, occupancy),
'B' => slider::attacks(from, &slider::DIAGONAL, target, occupancy),
'N' => leaper::attacks(from, &leaper::KNIGHT, target),
'I' => {
slider::attacks(from, &slider::DIAGONAL, target, occupancy)
|| leaper::attacks(from, &leaper::KNIGHT, target)
}
'E' => {
slider::attacks(from, &slider::ORTHOGONAL, target, occupancy)
|| leaper::attacks(from, &leaper::KNIGHT, target)
}
'T' => stepper::attacks(from, &gold_directions(side), target),
'P' | 'F' | 'S' => foot_soldier::attacks(variant, side, from, target),
_ => false,
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use super::{attackers_of, attacks_from, is_attacked};
use crate::domain::piece::Piece;
use crate::domain::side::Side;
use crate::domain::square::Square;
use crate::domain::variant::Variant;
use sashite_epin::Identifier as Epin;
fn sq(s: &str) -> Square {
Square::parse(s).expect("valid square")
}
fn piece(token: &str) -> Piece {
Piece::new(Epin::parse(token).expect("valid EPIN"))
}
fn board<'a>(squares: &'a [(&'a str, &'a str)]) -> impl Fn(Square) -> Option<Piece> + 'a {
move |s| {
squares
.iter()
.find(|(name, _)| sq(name) == s)
.map(|(_, tok)| piece(tok))
}
}
#[test]
fn rook_attacks_in_line_but_blocked() {
let b = board(&[("a1", "R")]);
assert!(is_attacked(sq("a8"), Side::First, Variant::Chess, &b));
let b2 = board(&[("a1", "R"), ("a4", "p")]);
assert!(!is_attacked(sq("a8"), Side::First, Variant::Chess, &b2));
assert!(is_attacked(sq("a4"), Side::First, Variant::Chess, &b2)); }
#[test]
fn queen_attacks_all_eight_directions() {
let b = board(&[("d4", "Q")]);
assert!(is_attacked(sq("d8"), Side::First, Variant::Chess, &b)); assert!(is_attacked(sq("a1"), Side::First, Variant::Chess, &b)); assert!(!is_attacked(sq("e6"), Side::First, Variant::Chess, &b)); }
#[test]
fn bishop_attacks_diagonally_only() {
let b = board(&[("d4", "B")]);
assert!(is_attacked(sq("a1"), Side::First, Variant::Chess, &b)); assert!(is_attacked(sq("h8"), Side::First, Variant::Chess, &b)); assert!(!is_attacked(sq("d8"), Side::First, Variant::Chess, &b)); }
#[test]
fn knight_leaps_over() {
let b = board(&[("d4", "N"), ("d5", "p"), ("d3", "p")]);
assert!(is_attacked(sq("e6"), Side::First, Variant::Chess, &b));
assert!(is_attacked(sq("f5"), Side::First, Variant::Chess, &b));
assert!(!is_attacked(sq("d6"), Side::First, Variant::Chess, &b));
}
#[test]
fn pawn_attacks_diagonally_not_straight() {
let b = board(&[("e4", "P")]);
assert!(is_attacked(sq("d5"), Side::First, Variant::Chess, &b));
assert!(is_attacked(sq("f5"), Side::First, Variant::Chess, &b));
assert!(!is_attacked(sq("e5"), Side::First, Variant::Chess, &b));
}
#[test]
fn king_attacks_adjacent_squares() {
let b = board(&[("e4", "K^")]);
assert!(is_attacked(sq("e5"), Side::First, Variant::Chess, &b));
assert!(is_attacked(sq("d3"), Side::First, Variant::Chess, &b));
assert!(!is_attacked(sq("e6"), Side::First, Variant::Chess, &b));
}
#[test]
fn flying_general_attacks_in_line() {
let b = board(&[("e1", "G^")]);
assert!(is_attacked(sq("e8"), Side::First, Variant::Xiongqi, &b));
assert!(!is_attacked(sq("f2"), Side::First, Variant::Xiongqi, &b));
}
#[test]
fn empress_combines_chariot_and_knight() {
let b = board(&[("d4", "E")]);
assert!(is_attacked(sq("d8"), Side::First, Variant::Xiongqi, &b)); assert!(is_attacked(sq("e6"), Side::First, Variant::Xiongqi, &b)); assert!(!is_attacked(sq("f6"), Side::First, Variant::Xiongqi, &b)); }
#[test]
fn princess_combines_bishop_and_knight() {
let b = board(&[("d4", "I")]);
assert!(is_attacked(sq("g7"), Side::First, Variant::Ogi, &b)); assert!(is_attacked(sq("e6"), Side::First, Variant::Ogi, &b)); assert!(!is_attacked(sq("d8"), Side::First, Variant::Ogi, &b)); }
#[test]
fn tokin_attacks_as_gold_general() {
let b = board(&[("e4", "T")]);
assert!(is_attacked(sq("e5"), Side::First, Variant::Ogi, &b)); assert!(is_attacked(sq("d5"), Side::First, Variant::Ogi, &b)); assert!(is_attacked(sq("e3"), Side::First, Variant::Ogi, &b)); assert!(!is_attacked(sq("d3"), Side::First, Variant::Ogi, &b)); }
#[test]
fn tokin_attacks_as_gold_general_second_side() {
let b = board(&[("e4", "t")]);
assert!(is_attacked(sq("e3"), Side::Second, Variant::Ogi, &b)); assert!(is_attacked(sq("d3"), Side::Second, Variant::Ogi, &b)); assert!(is_attacked(sq("e5"), Side::Second, Variant::Ogi, &b)); assert!(!is_attacked(sq("d5"), Side::Second, Variant::Ogi, &b)); }
#[test]
fn only_the_attacking_side_counts() {
let b = board(&[("a1", "r")]);
assert!(!is_attacked(sq("a8"), Side::First, Variant::Chess, &b));
assert!(is_attacked(sq("a8"), Side::Second, Variant::Chess, &b));
}
fn names(squares: &[Square]) -> Vec<String> {
squares.iter().map(ToString::to_string).collect()
}
#[test]
fn attackers_of_lists_every_attacker_and_only_them() {
let b = board(&[
("e1", "R"),
("a4", "R"),
("h4", "R"),
("f4", "p"),
("f6", "N"),
("b8", "B"),
("e8", "r"),
]);
let found = attackers_of(sq("e4"), Side::First, Variant::Chess, &b);
assert_eq!(names(&found), ["e1", "a4", "f6"]);
assert!(is_attacked(sq("e4"), Side::First, Variant::Chess, &b));
let theirs = attackers_of(sq("e4"), Side::Second, Variant::Chess, &b);
assert_eq!(names(&theirs), ["e8"]);
}
#[test]
fn attackers_of_counts_a_double_check() {
let b = board(&[("g8", "k^"), ("g1", "R"), ("f6", "N"), ("a1", "K^")]);
assert_eq!(
attackers_of(sq("g8"), Side::First, Variant::Chess, &b).len(),
2
);
}
#[test]
fn attackers_of_includes_a_defender_of_its_own_side() {
let b = board(&[("f6", "N"), ("h7", "R")]);
let found = attackers_of(sq("h7"), Side::First, Variant::Chess, &b);
assert_eq!(names(&found), ["f6"]);
}
#[test]
fn attackers_of_is_empty_exactly_when_is_attacked_is_false() {
let b = board(&[
("d4", "Q"),
("g1", "N"),
("c2", "P"),
("e5", "p"),
("h8", "k^"),
("b1", "K^"),
]);
for target in Square::all() {
for side in [Side::First, Side::Second] {
assert_eq!(
is_attacked(target, side, Variant::Chess, &b),
!attackers_of(target, side, Variant::Chess, &b).is_empty(),
"disagreement on {target} for {side:?}"
);
}
}
}
#[test]
fn attackers_of_reports_the_flying_general() {
let b = board(&[("e1", "G^"), ("e8", "g^")]);
assert_eq!(
names(&attackers_of(sq("e8"), Side::First, Variant::Xiongqi, &b)),
["e1"]
);
assert_eq!(
names(&attackers_of(sq("e1"), Side::Second, Variant::Xiongqi, &b)),
["e8"]
);
}
#[test]
fn attacks_from_is_the_per_piece_relation() {
let b = board(&[("a1", "R"), ("a4", "p"), ("h1", "R")]);
assert!(attacks_from(sq("a1"), Variant::Chess, sq("a4"), &b));
assert!(!attacks_from(sq("a1"), Variant::Chess, sq("a8"), &b));
assert!(attacks_from(sq("h1"), Variant::Chess, sq("a1"), &b));
}
#[test]
fn attacks_from_an_empty_square_is_false() {
let b = board(&[("a1", "R")]);
assert!(!attacks_from(sq("d4"), Variant::Chess, sq("d8"), &b));
}
#[test]
fn attacks_from_ignores_the_target_occupant() {
let b = board(&[("d4", "B"), ("f6", "P"), ("b6", "p")]);
assert!(attacks_from(sq("d4"), Variant::Chess, sq("f6"), &b));
assert!(attacks_from(sq("d4"), Variant::Chess, sq("b6"), &b));
}
#[test]
fn attacks_from_reads_the_variant_of_the_piece_on_the_square() {
let pawn = board(&[("e4", "P")]);
assert!(attacks_from(sq("e4"), Variant::Chess, sq("d5"), &pawn));
assert!(!attacks_from(sq("e4"), Variant::Chess, sq("e5"), &pawn));
let fu = board(&[("e4", "F")]);
assert!(attacks_from(sq("e4"), Variant::Ogi, sq("e5"), &fu));
assert!(!attacks_from(sq("e4"), Variant::Ogi, sq("d5"), &fu));
}
#[test]
fn attacks_from_agrees_with_attackers_of_on_every_pair() {
let b = board(&[
("d4", "I"),
("e6", "T"),
("c3", "F"),
("f2", "R"),
("g7", "b"),
("a8", "k^"),
("h1", "K^"),
]);
for target in Square::all() {
let listed = attackers_of(target, Side::First, Variant::Ogi, &b);
for from in Square::all() {
let owned = b(from).is_some_and(|p| p.belongs_to(Side::First));
let direct = owned && attacks_from(from, Variant::Ogi, target, &b);
assert_eq!(
listed.contains(&from),
direct,
"disagreement on {from} -> {target}"
);
}
}
}
}