///|
/// The 9x10 Xiangqi board plus game state metadata.
///
/// Squares are stored row-major with rank 0 (Red's back rank) first,
/// so `squares[pos.index()]` is the piece at `pos` (or `None` if empty).
pub struct Board {
squares : Array[Piece?]
side_to_move : Side
/// Number of full moves played, starting from 1.
move_number : Int
} derive(Eq, Debug)
///|
/// Build a board from a mutable square array.
pub fn Board::of_squares(
squares : Array[Piece?],
side_to_move : Side,
move_number : Int,
) -> Board {
{ squares, side_to_move, move_number, }
}
///|
/// The piece at `pos`, or `None` when the square is empty.
pub fn Board::get(self : Board, pos : Pos) -> Piece? {
if pos.is_valid() {
self.squares[pos.index()]
} else {
None
}
}
///|
/// The side whose turn it is.
pub fn Board::side(self : Board) -> Side {
self.side_to_move
}
///|
/// Locate the king of `side`. Kings are always on the board in
/// legal Xiangqi positions; returns `None` only for malformed boards.
pub fn Board::find_king(self : Board, side : Side) -> Pos? {
let (lo, hi) = if side.is_red() { (0, 2) } else { (7, 9) }
for rank in lo..<=hi {
for file in 3..<=5 {
let pos = Pos::new(file, rank)
match self.get(pos) {
Some(p) => if p.side == side && p.kind is King { return Some(pos) }
None => ()
}
}
}
None
}
///|
/// An empty board with Red to move. Combine with `with`, `without`
/// and `with_side` to construct custom test positions.
pub fn Board::empty() -> Board {
let squares : Array[Piece?] = Array::make(90, None)
{ squares, side_to_move: Red, move_number: 1, }
}
///|
/// The classic starting position. Red is at the bottom (ranks 0-4),
/// Black at the top (ranks 5-9). Red moves first.
pub fn Board::initial() -> Board {
let squares : Array[Piece?] = Array::make(90, None)
let back = [
Rook,
Horse,
Elephant,
Advisor,
King,
Advisor,
Elephant,
Horse,
Rook,
]
// Back ranks
for file in 0..<9 {
squares[Pos::new(file, 0).index()] = Some(Piece::red(back[file]))
squares[Pos::new(file, 9).index()] = Some(Piece::black(back[file]))
}
// Cannons on files b and h
for file in [1, 7] {
squares[Pos::new(file, 2).index()] = Some(Piece::red(Cannon))
squares[Pos::new(file, 7).index()] = Some(Piece::black(Cannon))
}
// Pawns on files a, c, e, g, i
for file in [0, 2, 4, 6, 8] {
squares[Pos::new(file, 3).index()] = Some(Piece::red(Pawn))
squares[Pos::new(file, 6).index()] = Some(Piece::black(Pawn))
}
{ squares, side_to_move: Red, move_number: 1, }
}
///|
/// Copy the square array (shared by the mutator helpers below).
fn Board::copy_squares(self : Board) -> Array[Piece?] {
let squares = Array::make(90, None)
for i in 0..<90 {
squares[i] = self.squares[i]
}
squares
}
///|
/// Apply a move and return the resulting board. The receiver is left
/// untouched. This function trusts that the move is pseudo-legal;
/// use `legal_moves` / `move_from_*` for validated play.
pub fn Board::apply_move(self : Board, mv : Move) -> Board {
let squares = self.copy_squares()
squares[mv.from.index()] = None
squares[mv.to.index()] = self.get(mv.from)
let next_side = self.side_to_move.opponent()
let move_number = if self.side_to_move.is_red() {
self.move_number
} else {
self.move_number + 1
}
{ squares, side_to_move: next_side, move_number, }
}
///|
/// Remove the piece at `pos` (for constructing custom positions).
pub fn Board::without(self : Board, pos : Pos) -> Board {
let squares = self.copy_squares()
squares[pos.index()] = None
{ squares, side_to_move: self.side_to_move, move_number: self.move_number, }
}
///|
/// Place or replace a piece at `pos` (for constructing custom positions).
pub fn Board::with_piece(self : Board, pos : Pos, piece : Piece) -> Board {
let squares = self.copy_squares()
squares[pos.index()] = Some(piece)
{ squares, side_to_move: self.side_to_move, move_number: self.move_number, }
}
///|
/// Set the side to move (for constructing custom positions).
pub fn Board::with_side(self : Board, side : Side) -> Board {
{ ..self, side_to_move: side, }
}
///|
/// Count all pieces currently on the board.
pub fn Board::piece_count(self : Board) -> Int {
let mut n = 0
for i in 0..<90 {
if self.squares[i] is Some(_) {
n = n + 1
}
}
n
}
///|
/// Collect every position occupied by a piece of `side` and `kind`.
pub fn Board::positions_of(
self : Board,
side : Side,
kind : Kind,
) -> Array[Pos] {
let result = []
for i in 0..<90 {
match self.squares[i] {
Some(p) =>
if p.side == side && p.kind == kind {
let rank = i / 9
let file = i % 9
result.push(Pos::new(file, rank))
}
None => ()
}
}
result
}
///|
/// Collect every position occupied by a piece of `side`.
pub fn Board::positions_of_side(self : Board, side : Side) -> Array[Pos] {
let result = []
for i in 0..<90 {
match self.squares[i] {
Some(p) =>
if p.side == side {
let rank = i / 9
let file = i % 9
result.push(Pos::new(file, rank))
}
None => ()
}
}
result
}