Sign in

    xiangqi

    中国象棋规则引擎基础库:走法生成、将军/将死判定、中文纵线记谱与 FEN 解析,零依赖可复用 / Zero-dependency Xiangqi (Chinese Chess) rules engine library for MoonBit

    xiangqi
    chinese-chess
    rules-engine
    move-generation
    fen
    notation
    Download zip
    Author
    Version
    0.2.3
    License
    Apache-2.0
    Last updated
    11 days ago
    Downloads
    72

    No dependencies

    #wbgxiaosu/xiangqi

    中国象棋规则引擎基础库 / A Xiangqi (Chinese Chess) rules engine library in pure MoonBit.

    • 完整走法生成(蹩马腿、塞象眼、炮架、过河兵、九宫约束)
    • 将军 / 将死 / 困毙判定,含将帅照面(飞将)规则
    • 中文纵线记谱(炮二平五、前车进一)与 ICCS 坐标双向转换
    • XiangqiFEN 解析与生成
    • UCCI 协议适配层(position 命令解析、着法回放)
    • 对局历史与长将禁着判定(长将判负规则)
    • Perft 验证:44 / 1920 / 79666,与 Chess Programming Wiki《Chinese Chess Perft Results》(CC BY-SA 3.0)初始局面参考值一致,并与 TalkChess 2026 年 1 月 Nguyen Pham 扩展数据(经 Pikafish 引擎交叉验证)吻合

    ///|
    let board = @xiangqi.Board::initial()

    ///|
    let mv = board.move_from_chinese("炮二平五").unwrap() // h2e2

    ///|
    let next = board.apply_move(mv)

    Run moon run cmd/main for a terminal demo. License: Apache-2.0.

    Board

    pub struct Board {
    squares : Array[Piece?]
    side_to_move : Side
    move_number : Int
    } derive(Eq,
    Debug
    )

    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).

    Board::apply_move

    fn Board::apply_move(self : Board, mv : Move) -> Board

    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.

    Board::empty

    fn Board::empty() -> Board

    An empty board with Red to move. Combine with with, without and with_side to construct custom test positions.

    Board::equal

    fn Board::equal(Board, Board) -> Bool

    Board::find_king

    fn Board::find_king(self : Board, side : Side) -> Pos?

    Locate the king of side. Kings are always on the board in legal Xiangqi positions; returns None only for malformed boards.

    Board::get

    fn Board::get(self : Board, pos : Pos) -> Piece?

    The piece at pos, or None when the square is empty.

    Board::in_check

    fn Board::in_check(self : Board, side : Side) -> Bool

    Is the king of side in check (including the flying-general rule)?

    Board::initial

    fn Board::initial() -> Board

    The classic starting position. Red is at the bottom (ranks 0-4), Black at the top (ranks 5-9). Red moves first.

    Board::is_attacked

    fn Board::is_attacked(self : Board, target : Pos, attacker : Side) -> Bool

    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.

    Board::is_checkmate

    fn Board::is_checkmate(self : Board) -> Bool

    True when the side to move is checkmated (in check with no legal response). Use Board::result for the complete outcome.

    Board::is_stalemate

    fn Board::is_stalemate(self : Board) -> Bool

    True when the side to move is stalemated (no legal moves while not in check — 困毙, which loses in Xiangqi).

    Board::kings_facing

    fn Board::kings_facing(self : Board) -> Bool

    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.

    Board::legal_moves

    fn Board::legal_moves(self : Board) -> Array[Move]

    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).

    Board::move_from_chinese

    fn Board::move_from_chinese(self : Board, notation : String) -> Result[Move, NotationError]

    Interpret a Chinese notation string against the current position. The implementation generates the notation of every legal move and selects the exact match, which guarantees round-trip consistency.

    Board::move_from_iccs

    fn Board::move_from_iccs(self : Board, iccs : String) -> Result[Move, NotationError]

    Parse an ICCS coordinate string into a move, validating that it is legal in the current position.

    Board::move_to_chinese

    fn Board::move_to_chinese(self : Board, mv : Move) -> String

    Render a move in Chinese file notation (中文纵线记谱法), e.g. 炮二平五, 马8进7, 前车进一. The board is required for disambiguation. Works for both the side to move and (for analysis tooling) the opposite side, as long as the moving piece belongs to board.side_to_move in the common case.

    Board::move_to_iccs

    fn Board::move_to_iccs(_self : Board, mv : Move) -> String

    Render a move in ICCS coordinate notation, e.g. h2e2. Files are a..i (a on Red's left), ranks 0..9 (0 is Red's back rank).

    Board::not_equal

    fn Board::not_equal(x : Board, y : Board) -> Bool

    Board::of_squares

    fn Board::of_squares(squares : Array[Piece?], side_to_move : Side, move_number : Int) -> Board

    Build a board from a mutable square array.

    Board::piece_count

    fn Board::piece_count(self : Board) -> Int

    Count all pieces currently on the board.

    Board::positions_of

    fn Board::positions_of(self : Board, side : Side, kind : Kind) -> Array[Pos]

    Collect every position occupied by a piece of side and kind.

    Board::positions_of_side

    fn Board::positions_of_side(self : Board, side : Side) -> Array[Pos]

    Collect every position occupied by a piece of side.

    Board::pseudo_moves

    fn Board::pseudo_moves(self : Board) -> Array[Move]

    Generate all pseudo-legal moves for the side to move.

    Board::pseudo_moves_from

    fn Board::pseudo_moves_from(self : Board, from : Pos) -> Array[Move]

    Generate all pseudo-legal moves for the piece at from. A pseudo-legal move follows the piece's movement pattern and does not land on a friendly piece; it may leave the mover's own king in check. Use legal_moves for fully validated moves.

    Board::render_board

    fn Board::render_board(self : Board) -> String

    Full board rendering including river and side-to-move caption.

    Board::result

    fn Board::result(self : Board) -> GameResult

    Evaluate the game status. In Xiangqi, a side with no legal moves loses whether or not it is in check (困毙 counts as a loss).

    Board::side

    fn Board::side(self : Board) -> Side

    The side whose turn it is.

    Board::to_fen

    fn Board::to_fen(self : Board) -> String

    Render a board as a FEN string in standard layout.

    Board::to_repr

    Board::with_piece

    fn Board::with_piece(self : Board, pos : Pos, piece : Piece) -> Board

    Place or replace a piece at pos (for constructing custom positions).

    Board::with_side

    fn Board::with_side(self : Board, side : Side) -> Board

    Set the side to move (for constructing custom positions).

    Board::without

    fn Board::without(self : Board, pos : Pos) -> Board

    Remove the piece at pos (for constructing custom positions).

    FenError

    pub enum FenError {
    BadFieldCount
    BadBoard(String)
    BadSide(String)
    BadMoveNumber(String)
    } derive(Eq,
    Debug
    )

    Errors produced while parsing a FEN string.

    FenError::equal

    fn FenError::equal(FenError, FenError) -> Bool

    FenError::not_equal

    fn FenError::not_equal(x : FenError, y : FenError) -> Bool

    FenError::to_repr

    FenError::to_string

    fn FenError::to_string(self : FenError) -> String

    Human-readable rendering of a FEN error (for messages and tests).

    GameHistory

    pub struct GameHistory {
    keys : Array[String]
    movers : Array[Side]
    checks : Array[Bool]
    } derive(Eq,
    Debug
    )

    Position history recorder. Start with new, then push the board after every move.

    GameHistory::check_streak

    fn GameHistory::check_streak(self : GameHistory, side : Side) -> Int

    Length of the side's current streak of consecutive checking moves, counted backwards from the latest position. Any own move that did not give check resets the streak.

    GameHistory::equal

    fn GameHistory::equal(GameHistory, GameHistory) -> Bool

    GameHistory::long_check_side

    fn GameHistory::long_check_side(self : GameHistory, min_repeats : Int, min_streak : Int) -> Side?

    Simplified long-check adjudication: a side violates the rule when it has delivered check on at least min_streak consecutive own moves while the current position has repeated at least min_repeats times. Returns the offending side, if any. With the competition thresholds this corresponds to 长将判负; callers decide the limits.

    GameHistory::new

    fn GameHistory::new(start : Board) -> GameHistory

    Start a history from any position.

    GameHistory::not_equal

    fn GameHistory::not_equal(x : GameHistory, y : GameHistory) -> Bool

    GameHistory::position_repetitions

    fn GameHistory::position_repetitions(self : GameHistory) -> Int

    How many times the current (latest) position has occurred in the recorded history, inclusive.

    GameHistory::push

    fn GameHistory::push(self : GameHistory, after : Board) -> Unit

    Record one played move by passing the board after the move. The mover and the check flag are derived from the board itself.

    GameResult

    pub(all) enum GameResult {
    Ongoing
    RedWins
    BlackWins
    } derive(Eq,
    Debug
    )

    Outcome of a position from the perspective of the game.

    GameResult::equal

    fn GameResult::equal(GameResult, GameResult) -> Bool

    GameResult::not_equal

    fn GameResult::not_equal(x : GameResult, y : GameResult) -> Bool

    Kind

    pub(all) enum Kind {
    King
    Advisor
    Elephant
    Horse
    Rook
    Cannon
    Pawn
    } derive(Eq,
    Debug
    )

    The seven Xiangqi piece kinds.

    Kind::equal

    fn Kind::equal(Kind, Kind) -> Bool

    Kind::not_equal

    fn Kind::not_equal(x : Kind, y : Kind) -> Bool

    Kind::to_repr

    Move

    pub struct Move {
    from : Pos
    to : Pos
    } derive(Eq,
    Debug
    )

    A move from from to to.

    Move::equal

    fn Move::equal(Move, Move) -> Bool

    Move::new

    fn Move::new(from : Pos, to : Pos) -> Move

    Move::not_equal

    fn Move::not_equal(x : Move, y : Move) -> Bool

    Move::to_repr

    NotationError

    pub enum NotationError {
    IllegalOrUnknown(String)
    BadIccs(String)
    } derive(Eq,
    Debug
    )

    Errors from coordinate parsing.

    NotationError::equal

    NotationError::not_equal

    fn NotationError::not_equal(x : NotationError, y : NotationError) -> Bool

    NotationError::to_string

    fn NotationError::to_string(self : NotationError) -> String

    Human-readable rendering of a notation error (for messages and tests).

    Piece

    pub struct Piece {
    side : Side
    kind : Kind
    } derive(Eq,
    Debug
    )

    A single piece on the board.

    Piece::black

    fn Piece::black(kind : Kind) -> Piece

    Piece::equal

    fn Piece::equal(Piece, Piece) -> Bool

    Piece::not_equal

    fn Piece::not_equal(x : Piece, y : Piece) -> Bool

    Piece::red

    fn Piece::red(kind : Kind) -> Piece

    Constructors for the two sides.

    Piece::to_repr

    Pos

    pub struct Pos {
    file : Int
    rank : Int
    } derive(Eq,
    Debug
    )

    Board position. file is 0..8 (files a..i, a on Red's left), rank is 0..9 (rank 0 is Red's back rank, rank 9 is Black's).

    Pos::equal

    fn Pos::equal(Pos, Pos) -> Bool

    Pos::in_palace

    fn Pos::in_palace(self : Pos, side : Side) -> Bool

    True when the position is inside the palace of side.

    Pos::index

    fn Pos::index(self : Pos) -> Int

    Linear index into the 90-square board array.

    Pos::is_valid

    fn Pos::is_valid(self : Pos) -> Bool

    True when the position lies on the 9x10 board.

    Pos::new

    fn Pos::new(file : Int, rank : Int) -> Pos

    Pos::not_equal

    fn Pos::not_equal(x : Pos, y : Pos) -> Bool

    Pos::own_half

    fn Pos::own_half(self : Pos, side : Side) -> Bool

    True when the position is on side's own half of the river. The river lies between rank 4 and rank 5.

    Pos::to_repr

    Side

    pub(all) enum Side {
    Red
    Black
    } derive(Eq,
    Debug
    )

    Side of a piece or the player to move. Red moves first and advances from rank 0 towards rank 9.

    Side::equal

    fn Side::equal(Side, Side) -> Bool

    Side::is_red

    fn Side::is_red(self : Side) -> Bool

    Side::not_equal

    fn Side::not_equal(x : Side, y : Side) -> Bool

    Side::opponent

    fn Side::opponent(self : Side) -> Side

    The opposing side.

    Side::to_repr

    char_to_piece_kind

    fn char_to_piece_kind(ch : Char) -> Kind?

    Map a notation character to a piece kind (accepts simplified and traditional variants used by common software).

    parse_fen

    fn parse_fen(fen : String) -> Result[Board, FenError]

    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.

    perft

    fn perft(board : Board, depth : Int) -> Int

    Perft: count leaf nodes of the legal move tree to depth. Used to validate move generation against known reference counts.