xiangqi

    中国象棋规则引擎:完整的走法生成、将死/困毙判定、中文纵线记谱与 FEN 支持 / A Xiangqi (Chinese Chess) rules engine in pure MoonBit

    xiangqi
    chinese-chess
    board-game
    game-engine
    rules
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    4 hours ago
    Downloads
    1

    #wbgxiaosu/xiangqi

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

    • 完整走法生成(蹩马腿、塞象眼、炮架、过河兵、九宫约束)
    • 将军 / 将死 / 困毙判定,含将帅照面(飞将)规则
    • 中文纵线记谱(炮二平五、前车进一)与 ICCS 坐标双向转换
    • XiangqiFEN 解析与生成
    • Perft 验证:44 / 1920 / 79666,与公开参考值一致

    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::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::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::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, Show,
    Debug
    )

    Errors produced while parsing a FEN string.

    GameResult

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

    Outcome of a position from the perspective of the game.

    Kind

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

    The seven Xiangqi piece kinds.

    Move

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

    A move from from to to.

    Move::new

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

    NotationError

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

    Errors from coordinate parsing.

    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::red

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

    Constructors for the two sides.

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

    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::is_red

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

    Side::opponent

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

    The opposing side.

    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.