///|
/// Is `target` attacked by any piece of `attacker`? Includes rook,
/// cannon (with screen), horse (with leg check) and pawn attacks.
/// The flying-general rule is checked separately by `kings_facing`.
pub fn Board::is_attacked(self : Board, target : Pos, attacker : Side) -> Bool {
  if self.rook_attacks(target, attacker) {
    return true
  }
  if self.cannon_attacks(target, attacker) {
    return true
  }
  if self.horse_attacks(target, attacker) {
    return true
  }
  if self.pawn_attacks(target, attacker) {
    return true
  }
  false
}

///|
/// Rook-style line attack: the first piece seen on any ray from
/// `target` is an enemy rook.
fn Board::rook_attacks(self : Board, target : Pos, attacker : Side) -> Bool {
  let dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
  for dir in dirs {
    let mut f = target.file + dir.0
    let mut r = target.rank + dir.1
    while Pos::new(f, r).is_valid() {
      match self.get(Pos::new(f, r)) {
        None => ()
        Some(p) =>
          if p.side == attacker && p.kind is Rook {
            return true
          } else {
            break
          }
      }
      f = f + dir.0
      r = r + dir.1
    }
  }
  false
}

///|
/// Cannon attack: past the first screen on a ray, the first piece
/// found is an enemy cannon.
fn Board::cannon_attacks(self : Board, target : Pos, attacker : Side) -> Bool {
  let dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
  for dir in dirs {
    let mut f = target.file + dir.0
    let mut r = target.rank + dir.1
    let mut screen_seen = false
    while Pos::new(f, r).is_valid() {
      match self.get(Pos::new(f, r)) {
        None => ()
        Some(p) =>
          if !screen_seen {
            screen_seen = true
          } else {
            if p.side == attacker && p.kind is Cannon {
              return true
            }
            break
          }
      }
      f = f + dir.0
      r = r + dir.1
    }
  }
  false
}

///|
/// Horse attack. The blocking leg sits next to the *attacking* horse,
/// so from the target we check the leg square adjacent to the
/// candidate horse position.
fn Board::horse_attacks(self : Board, target : Pos, attacker : Side) -> Bool {
  for pair in horse_offsets() {
    let (offset, leg) = pair
    // Horse at (target - offset) attacks target if its leg is empty.
    let horse_pos = Pos::new(target.file - offset.0, target.rank - offset.1)
    if horse_pos.is_valid() {
      match self.get(horse_pos) {
        Some(p) =>
          if p.side == attacker && p.kind is Horse {
            let leg_pos = Pos::new(
              horse_pos.file + leg.0,
              horse_pos.rank + leg.1,
            )
            if self.get(leg_pos) is None {
              return true
            }
          }
        None => ()
      }
    }
  }
  false
}

///|
/// Pawn attack: an enemy pawn directly in front of `target`, or on
/// either side once that pawn has crossed the river.
fn Board::pawn_attacks(self : Board, target : Pos, attacker : Side) -> Bool {
  // Pawn directly in front (from the attacker's perspective).
  let front = Pos::new(target.file, target.rank - forward_step(attacker))
  if front.is_valid() {
    match self.get(front) {
      Some(p) => if p.side == attacker && p.kind is Pawn { return true }
      None => ()
    }
  }
  // Pawns on the flanks, but only after they have crossed the river.
  for df in [-1, 1] {
    let side_pos = Pos::new(target.file + df, target.rank)
    if side_pos.is_valid() {
      match self.get(side_pos) {
        Some(p) =>
          if p.side == attacker &&
            p.kind is Pawn &&
            !side_pos.own_half(attacker) {
            return true
          }
        None => ()
      }
    }
  }
  false
}

///|
/// The flying-general rule: the two kings face each other on the same
/// file with no piece in between. Such a position is illegal for the
/// side that just moved.
pub fn Board::kings_facing(self : Board) -> Bool {
  let red_king = self.find_king(Red)
  let black_king = self.find_king(Black)
  match (red_king, black_king) {
    (Some(rk), Some(bk)) =>
      if rk.file == bk.file {
        let mut r = rk.rank + 1
        while r < bk.rank {
          if self.get(Pos::new(rk.file, r)) is Some(_) {
            return false
          }
          r = r + 1
        }
        true
      } else {
        false
      }
    _ => false
  }
}

///|
/// Is the king of `side` in check (including the flying-general rule)?
pub fn Board::in_check(self : Board, side : Side) -> Bool {
  match self.find_king(side) {
    Some(king_pos) =>
      self.is_attacked(king_pos, side.opponent()) || self.kings_facing()
    None => false
  }
}

///|
/// All fully legal moves for the side to move: pseudo-legal moves that
/// leave the mover's own king safe (not in check, kings not facing).
pub fn Board::legal_moves(self : Board) -> Array[Move] {
  let result = []
  let side = self.side_to_move
  for mv in self.pseudo_moves() {
    let next = self.apply_move(mv)
    if !next.in_check(side) {
      result.push(mv)
    }
  }
  result
}

///|
/// Outcome of a position from the perspective of the game.
pub(all) enum GameResult {
  /// The game is still in progress.
  Ongoing
  /// Red has won (checkmate or stalemate against Black).
  RedWins
  /// Black has won (checkmate or stalemate against Red).
  BlackWins
} derive(Eq, Debug)

///|
/// Evaluate the game status. In Xiangqi, a side with no legal moves
/// loses whether or not it is in check (困毙 counts as a loss).
pub fn Board::result(self : Board) -> GameResult {
  let side = self.side_to_move
  if self.legal_moves().length() == 0 {
    if side.is_red() {
      BlackWins
    } else {
      RedWins
    }
  } else {
    Ongoing
  }
}

///|
/// True when the side to move is checkmated (in check with no legal
/// response). Use `Board::result` for the complete outcome.
pub fn Board::is_checkmate(self : Board) -> Bool {
  let side = self.side_to_move
  self.in_check(side) && self.legal_moves().length() == 0
}

///|
/// True when the side to move is stalemated (no legal moves while not
/// in check — 困毙, which loses in Xiangqi).
pub fn Board::is_stalemate(self : Board) -> Bool {
  let side = self.side_to_move
  !self.in_check(side) && self.legal_moves().length() == 0
}

///|
/// Perft: count leaf nodes of the legal move tree to `depth`.
/// Used to validate move generation against known reference counts.
pub fn perft(board : Board, depth : Int) -> Int {
  if depth <= 0 {
    return 1
  }
  let moves = board.legal_moves()
  if depth == 1 {
    return moves.length()
  }
  let mut total = 0
  for mv in moves {
    total = total + perft(board.apply_move(mv), depth - 1)
  }
  total
}