///|
/// Errors produced while parsing a FEN string.
pub enum FenError {
/// The string does not have 4+ space-separated fields.
BadFieldCount
/// The board layout field is malformed.
BadBoard(String)
/// The side-to-move field is not `w` or `b`.
BadSide(String)
/// The move number field is not a non-negative integer.
BadMoveNumber(String)
} derive(Eq, Show, Debug)
///|
fn char_to_kind(c : Char) -> Kind? {
match c {
'K' | 'k' => Some(King)
'A' | 'a' => Some(Advisor)
'B' | 'b' | 'E' | 'e' => Some(Elephant)
'N' | 'n' | 'H' | 'h' => Some(Horse)
'R' | 'r' => Some(Rook)
'C' | 'c' => Some(Cannon)
'P' | 'p' => Some(Pawn)
_ => None
}
}
///|
fn is_upper(c : Char) -> Bool {
c >= 'A' && c <= 'Z'
}
///|
fn digit_value(c : Char) -> Int {
c.to_int() - '0'.to_int()
}
///|
/// Split a string on a single separator character.
fn split_char(s : String, sep : Char) -> Array[String] {
let parts = []
let mut current = StringBuilder()
for ch in s {
if ch == sep {
parts.push(current.to_string())
current = StringBuilder()
} else {
current.write_char(ch)
}
}
parts.push(current.to_string())
parts
}
///|
/// Split a string on single spaces (FEN separator).
fn split_spaces(s : String) -> Array[String] {
let raw = split_char(s, ' ')
let parts = []
for p in raw {
if p.length() > 0 {
parts.push(p)
}
}
parts
}
///|
/// Parse a Xiangqi FEN string, e.g.
/// `"rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1"`.
///
/// Ranks are listed top-down: the first row is rank 9 (Black's back
/// rank), the last row is rank 0 (Red's back rank). Letters are
/// standard XiangqiFEN: `k a b n r c p` (case = side). The second
/// field is `w` (Red) or `b` (Black); the last field, when present,
/// is the full-move number.
pub fn parse_fen(fen : String) -> Result[Board, FenError] {
let parts = split_spaces(fen)
if parts.length() < 2 {
return Err(BadFieldCount)
}
// Board layout
let rows = split_char(parts[0], '/')
if rows.length() != 10 {
return Err(BadBoard(parts[0]))
}
let squares : Array[Piece?] = Array::make(90, None)
for row_idx in 0..<10 {
let rank = 9 - row_idx
let row = rows[row_idx]
let mut file = 0
for ch in row {
if ch >= '1' && ch <= '9' {
file = file + digit_value(ch)
} else {
match char_to_kind(ch) {
Some(kind) => {
if file > 8 {
return Err(BadBoard(row))
}
let side = if is_upper(ch) { Red } else { Black }
squares[Pos::new(file, rank).index()] = Some(Piece::{ side, kind, })
file = file + 1
}
None => return Err(BadBoard(row))
}
}
}
if file != 9 {
return Err(BadBoard(row))
}
}
// Side to move
let side = match parts[1] {
"w" | "r" => Red
"b" => Black
other => return Err(BadSide(other))
}
// Move number (last field when present)
let move_number = if parts.length() >= 5 {
let field = parts[parts.length() - 1]
match parse_int(field) {
Some(n) => n
None => return Err(BadMoveNumber(field))
}
} else {
1
}
Ok(Board::of_squares(squares, side, move_number))
}
///|
fn parse_int(s : String) -> Int? {
if s.length() == 0 {
return None
}
let mut n = 0
for ch in s {
if ch < '0' || ch > '9' {
return None
}
n = n * 10 + digit_value(ch)
}
Some(n)
}
///|
/// Render a board as a FEN string in standard layout.
pub fn Board::to_fen(self : Board) -> String {
let sb = StringBuilder()
for row_idx in 0..<10 {
let rank = 9 - row_idx
if row_idx > 0 {
sb.write_char('/')
}
let mut empties = 0
for file in 0..<9 {
match self.get(Pos::new(file, rank)) {
Some(piece) => {
if empties > 0 {
sb.write_char(digit_char(empties))
empties = 0
}
sb.write_char(fen_char(piece))
}
None => empties = empties + 1
}
}
if empties > 0 {
sb.write_char(digit_char(empties))
}
}
sb.write_char(' ')
sb.write_string(if self.side_to_move.is_red() { "w" } else { "b" })
sb.write_string(" - - ")
sb.write_string(self.move_number.to_string())
sb.to_string()
}
///|
fn digit_char(n : Int) -> Char {
// n is always 1..9 here.
('0'.to_int() + n).unsafe_to_char()
}
///|
fn fen_char(piece : Piece) -> Char {
let c = match piece.kind {
King => 'k'
Advisor => 'a'
Elephant => 'b'
Horse => 'n'
Rook => 'r'
Cannon => 'c'
Pawn => 'p'
}
if piece.side.is_red() {
(c.to_int() - 32).unsafe_to_char()
} else {
c
}
}