python-chinese-chess

Python-Chinese-Chess

这是一个使用纯Python编写的中国象棋库,改写自Python-Chess项目的核心部分。

GPL-3.0 Python 3.7

特点与说明

基本操作

>>> import cchess
>>> import cchess.svg

>>> board = cchess.Board()

>>> board.push(cchess.Move.from_uci("h2h4"))
>>> board.push(cchess.Move.from_uci('d9e8'))
>>> board.push(cchess.Move.from_uci('h4g4'))
>>> board.push(cchess.Move.from_uci('g6g5'))
>>> board.push(cchess.Move.from_uci('g4g9'))

>>> board.is_checkmate()
True

>>> board
Board('rnb1kaCnr/4a4/1c5c1/p1p1p3p/6p2/9/P1P1P1P1P/1C7/9/RNBAKABNR b - - 0 3')

>>> svg = cchess.svg.board(board, size=600, coordinates=True, orientation=cchess.RED, lastmove=board.peek(), checkers=board.checkers())

安装方法

```shell script git clone https://github.com/windshadow233/python-chinese-chess.git


## 功能

### svg(两种坐标系)棋盘渲染
```python
>>> svg = cchess.svg.board(board,  # 渲染的棋盘
                           size=600,  # 棋盘尺寸
                           coordinates=True,  # 是否显示坐标系
                           axes_type=1,  # 坐标轴类型
                           lastmove=board.peek(),  # 上一步
                           checkers=board.checkers(),  # 将军棋子
                           orientation=cchess.RED,  # 棋盘方向
                           style="#board{fill:#1b91bc}"  # style-sheet
                           )
>>> board = cchess.Board("9/9/9/9/9/9/9/9/9/9")
>>> cchess.svg.board(board, squares=cchess.SquareSet([cchess.E4, cchess.F8]))

>>> board = cchess.Board("9/9/9/9/9/4NR3/9/9/9/9")
>>> cchess.svg.board(board, squares=board.attacks(cchess.E4))
>>> cchess.svg.board(board, squares=board.attacks(cchess.F4))
>>> board = cchess.Board()
>>> board.push(cchess.Move.from_uci("h2h4"))  # 行一步棋
>>> board.pop()  # 撤销上一步棋
Move.from_uci("h2h4")

字符串棋盘打印

>>> print(board)
r n b a k a b n r
. . . . . . . . .
. c . . . . . c .
p . p . p . p . p
. . . . . . . . .
. . . . . . . . .
P . P . P . P . P
. C . . . . . C .
. . . . . . . . .
R N B A K A B N R
>>> print(board.unicode(axes=True, axes_type=0))
  abcdefghi
9 車馬象士將士象馬車
8 .........
7 .....
6 
5 .........
4 .........
3 
2 .....
1 .........
0 俥傌相仕帥仕相傌俥
  abcdefghi

>>> print(board.unicode(axes=True, axes_type=1))
123456789
車馬象士將士象馬車
.........
.....

.........
.........

.....
.........
俥傌相仕帥仕相傌俥
九八七六五四三二一

(伪)合法着法生成、合法性判断

>>> board = cchess.Board()
>>> legal_moves = board.legal_moves
>>> board.legal_moves
<LegalMoveGenerator at ... (i3i4, g3g4, e3e4, c3c4, ...)>
>>> cchess.Move.from_uci("h2h8") in board.legal_moves
False
>>> cchess.Move.from_uci("h2h9") in board.legal_moves
True
>>> board.is_legal(cchess.Move.from_uci("h2h9"))
True

>>> board = cchess.Board('4k3R/2N2n3/5N3/9/9/9/9/9/9/3K5 b')
>>> board.pseudo_legal_moves
<PseudoLegalMoveGenerator at ... (e9f9, e9d9, e9e8, f8h9, f8d9, f8h7, f8d7)>
>>> board.is_pseudo_legal(cchess.Move.from_uci("e9d9"))
True
>>> board.is_legal(cchess.Move.from_uci("e9d9"))
False

攻击(特殊情况:将军)检测

>>> board= cchess.Board('4k4/2N6/9/9/9/9/9/9/9/3K5 b')
>>> board.is_attacked_by(cchess.RED, cchess.E9)
True
>>> board.is_check()
True

攻击者检测

>>> board = cchess.Board('4k3R/2N2n3/5N3/9/9/9/9/9/9/3K5 b')
>>> attackers = board.attackers(cchess.RED, cchess.E9)
>>> attackers
SquareSet(0x20004000000000000000000)
>>> print(attackers)
. . . . . . . . 1
. . 1 . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .

终局判断

>>> board = cchess.Board('rnb1kaCnr/4a4/1c5c1/p1p1p3p/6p2/9/P1P1P1P1P/1C7/9/RNBAKABNR b - - 0 3')
>>> board.is_checkmate()
True

>>> board = cchess.Board('3k5/R8/9/9/9/9/9/9/9/4K4 b')
>>> board.is_checkmate()
False
>>> board.is_stalemate()
True

>>> board = cchess.Board('2b1k4/9/4b4/9/9/9/9/9/4A4/4KA3')
>>> board.is_insufficient_material()
True
>>> board = cchess.Board('rnb1kaCnr/4a4/1c5c1/p1p1p3p/6p2/9/P1P1P1P1P/1C7/9/RNBAKABNR b - - 0 3')
>>> board.is_game_over()  # 简单判断是否结束
True
>>> board.outcome()  # 棋局的结束状态(若非终局则返回None)
Outcome(termination=<Termination.CHECKMATE: 1>, winner=True)

局面合法性检验

包含棋子数量、棋子位置、将帅照面等情况

>>> board = cchess.Board('3k5/R8/9/9/9/9/9/9/9/4K4')
>>> board.status()
<Status.VALID: 0>

>>> board = cchess.Board('4k4/9/9/9/9/9/9/9/9/4K4')
>>> board.is_king_line_of_sight()
True
>>> board.status()
<Status.KING_LINE_OF_SIGHT: 268435456>

长将检测

单方连续将军,形成三次以上(允许三次)循环局面时,判为“长将”。判定方法比较naive。

>>> board = cchess.Board("3akn3/5c3/1P2b4/4R4/9/9/9/9/9/4CK3")
>>> board.push_uci("e6e7")  # 第一着有吃子,不计入将军次数
>>> board.push_uci("f8e8")
>>> board.push_uci("e7f7")
>>> board.push_uci("e8f8")
>>> board.push_uci("f7e7")
>>> board.push_uci("f8e8")
>>> board.push_uci("e7f7")  # 此时尚未构成长将,但下一着若仍继续循环将军,则构成长将
>>> board.is_perpetual_check()
False
>>> board.push_uci("e8f8")
>>> board.push_uci("f7e7")  # 红方连续将军造成循环局面
>>> board.is_perpetual_check()
True

>>> board = cchess.Board("3akr3/5c3/1P2b4/4R4/9/9/9/9/9/4CK3")
>>> board.push_uci("e6e7")
>>> board.push_uci("f8e8")  # 黑方存在解将还将
>>> board.push_uci("e7f7")
>>> board.push_uci("e8f8")
>>> board.push_uci("f7e7")
>>> board.push_uci("f8e8")
>>> board.push_uci("e7f7")
>>> board.is_perpetual_check()  # 双方交替长将,故此时红方不构成长将
False

>>> board = cchess.Board("5c3/9/5k3/9/5N3/9/5c3/9/5K3/9")
>>> board.push_uci("f5d6")
>>> board.push_uci("f7e7")  # 黑方平將存在解将还将
>>> board.push_uci("d6f5")
>>> board.push_uci("e7f7")
>>> board.push_uci("f5d6")
>>> board.push_uci("f7e7")
>>> board.push_uci("d6f5")
>>> board.is_perpetual_check()  # 黑方虽有解将还将,但属于一将一闲,故红方仍构成长将
True

>>> board = cchess.Board("4R4/3k5/5R3/9/9/9/9/9/4A4/4K4")
>>> board.push_notation("车四进一")
>>> board.push_notation("将4进1")
>>> board.push_notation("车五平六")
>>> board.push_notation("将4平5")
>>> board.push_notation("车六平五")
>>> board.push_notation("将5平4")
>>> board.push_notation("车四退一")
>>> board.push_notation("将4退1")
>>> board.push_notation("车四进一")  # 红方两子交替循环将军,构成长将
>>> board.is_perpetual_check()
True

循环局面检测

>>> board.is_fourfold_repetition()  # 一般比赛规定出现三次循环局面后,不变着作和
False
>>> n = 7
>>> board.is_repetition(n)  # 也可以根据情况任意指定不变作和需要达到的循环次数
False

自然限着检测

>>> board.is_sixty_moves()  # 一般比赛规定60回合为自然限着数
False
>>> n = 75
>>> board.is_halfmoves(2 * n)  # 也可以根据情况任意指定限着数
False

四字记谱法

四字记谱法说明:

以棋子对应的Unicode字符(中文汉字)指明棋子类别,红黑方分别以中文数字“一~九”、阿拉伯数字“1~9”作为纵坐标,例如“炮二”表示二路红炮、“馬8”表示8路黑马。

第三字为“进”、“退”或“平”,第四字为一个数字(红方为汉字数字“一~九”,黑方为阿拉伯数字“1~9”),其中,只有“俥(車)”、“炮(砲)”、“兵(卒)”、“帥(將)”可以使用“平”,表示横向的移动,此时第四字表示移动到的列坐标,例如“車2平4”表示2路黑车平到4路、“炮二平五”表示二路红炮平到五路。 如第三字为“进”、“退”,第四字根据棋子类别具有不同的意义,若为“俥(車)”、“炮(砲)”、“兵(卒)”、“帥(將)”,则表示其在同一纵线上行走的步数,例如“帥五进一”表示五路红方将领前进一步、“炮2进1”表示2路黑炮前进一步;若为“傌(馬)”、“相(象)”、“仕(士)”,则第四字为到达的纵线坐标,例如“馬8进7”表示8路黑马前进至7路线、“相三进五”表示三路红相前进至五路线。

两个相同的“俥(車)”、“傌(馬)”、“炮(砲)”、“兵(卒)”位于同一列的情况,以“前”、“后”区分,如“前車”、“后砲”。

“仕(士)”、“相(象)”即使是两个在同一列的情况,也不需要以“前”、“后”区分,因第三字“进”、“退”足以区分位置。

对于“兵(卒)”,可能存在同一纵线上超过两个的情况,如同一条纵线上存在三个“兵(卒)”,则以“前”、“中”、“后”区分; 如同一纵线上存在四个“兵(卒)”,则以“前”、“二”、“三”、“四”区分; 如同一纵线上存在五个“兵(卒)”,则以“前”、“二”、“三”、“四”、“五”区分。 另外,如同时存在两条纵线上有2或3个“兵(卒)”,则需要加上纵坐标,如“前兵三进一”表示“三”路线的前红兵向前走一步,“中卒2平3”表示“2”路线的中黑卒平到“3”路线。 为维持四字记谱法,上面这种情况下,一般将“兵(卒)”省略,即记为“前三进一”、“中2平3”。

>>> board = cchess.Board()
>>> board.move_to_notation(cchess.Move.from_uci("h2e2"))
'炮二平五'
>>> board.move_to_notation(cchess.Move.from_uci("i9i7"))
'車9进2'
>>> board = cchess.Board("4k4/9/9/9/C8/9/C8/9/9/3K5")
>>> board.move_to_notation(cchess.Move.from_uci("a3a4"))
'后炮进一'
>>> board.move_to_notation(cchess.Move.from_uci("a5e5"))
'前炮平五'
>>> board = cchess.Board("4k4/6P2/1P7/1P4P2/1P7/n8/n8/9/9/3K5")
>>> board.move_to_notation(cchess.Move.from_uci("g8h8"))
'前三平二'
>>> board.move_to_notation(cchess.Move.from_uci("b5c5"))
'后八平七'
>>> board.parse_notation("中八平九")
Move.from_uci('b6a6')
>>> board.parse_notation("前馬进3")
Move.from_uci('a3c2')
>>> board.parse_notation("后馬退2")
Move.from_uci('a4b6')
>>> board = cchess.Board()
 
>>> board.push_notation('炮二平五')
Move.from_uci('h2e2')
>>> board.push_notation('砲8平5')
Move.from_uci('h7e7')
>>> board.push_notation('傌二进三')
Move.from_uci('h0g2')
>>> board.push_notation('馬8进7')
Move.from_uci('h9g7')
>>> board.push_notation('俥一平二')
Move.from_uci('i0h0')
>>> board.push_notation('車9进1')
Move.from_uci('i9i8')
>>> board.push_notation('傌八进七')
Move.from_uci('b0c2')
>>> board.push_notation('車9平4')
Move.from_uci('i8d8')
>>> board.push_notation('兵三进一')
Move.from_uci('g3g4')
>>> board.push_notation('車4进4')
Move.from_uci('d8d4')
>>> board.push_notation('俥二进四')
Move.from_uci('h0h4')
>>> board.push_notation('馬2进1')
Move.from_uci('b9a7')
>>> board.push_notation('炮五平四')
Move.from_uci('e2f2')
>>> board.push_notation('砲2平3')
Move.from_uci('b7c7')
>>> board.push_notation('相三进五')
Move.from_uci('g0e2')
>>> board.push_notation('車1平2')
Move.from_uci('a9b9')
>>> board.push_notation('俥九平八')
Move.from_uci('a0b0')
>>> board.push_notation('車2进4')
Move.from_uci('b9b5')
>>> board.push_notation('兵七进一')
Move.from_uci('c3c4')
>>> board.push_notation('車4进1')
Move.from_uci('d4d3')
>>> board.push_notation('傌三进四')
Move.from_uci('g2f4')
>>> board.push_notation('砲5进4')
Move.from_uci('e7e3')
>>> board.push_notation('仕四进五')
Move.from_uci('f0e1')
>>> board.push_notation('車4平3')
Move.from_uci('d3c3')
>>> board.push_notation('兵七进一')
Move.from_uci('c4c5')
>>> board.push_notation('車2平3')
Move.from_uci('b5c5')
>>> board.push_notation('炮八退一')
Move.from_uci('b2b1')
>>> board.push_notation('前車平2')
Move.from_uci('c3b3')
>>> board.push_notation('炮八平七')
Move.from_uci('b1c1')
>>> board.push_notation('車2进3')
Move.from_uci('b3b0')
>>> board.push_notation('炮七进四')
Move.from_uci('c1c5')
>>> board.push_notation('卒3进1')
Move.from_uci('c6c5')
>>> board.push_notation('傌七进五')
Move.from_uci('c2e3')
>>> board = cchess.Board()

>>> board.push(cchess.Move.from_uci("h2h4"))
>>> board.push(cchess.Move.from_uci('d9e8'))
>>> board.push(cchess.Move.from_uci('h4g4'))
>>> board.push(cchess.Move.from_uci('g6g5'))
>>> board.push(cchess.Move.from_uci('g4g9'))

>>> print(board.to_pgn())
[Game "Chinese Chess"]
[Round: "3"]
[PlyCount "5"]
[Date "2022-01-26"]
[Red ""]
[Black ""]
[Generator: "Python-Chinese-Chess"]
[Result "1-0"]
[FEN "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1"]
1.炮二进二 士4进5
2.炮二平三 卒7进1
3.炮三进五
1-0 {红胜}

使用此功能需要预先安装numpy, pillow, cairosvg, imageio 与 tqdm库

>>> import cchess.svg
>>> cchess.svg.to_gif(board, "images/board.gif",
                      duration=2,  # 每一步的时间间隔
                      orientation=cchess.RED,  # 棋盘方向
                      coordinates=True,  # 是否显示坐标
                      axes_type=1,  # 坐标轴类型
                      lastmove=True,  # 是否显示上一步
                      checkers=True,  # 是否显示将军棋子
                      size=450  # 棋盘尺寸
                      )

此项目只实现了Chinese(中文纵线格式)和ICCS(ICCS坐标格式)的PGN文件解析,可通过“[Format]”字段指定,例如“[Format “Chinese”]”,默认为Chinese。

一个合法的PGN文件应该与示例(pgn/cg245.pgn)类似,除了一些棋局信息元数据,还必须包含“[FEN]”字段(棋局初始状态)。

在“[FEN]”字段之后进行着法的记录,每一行至多记录两条以“四字记谱法(中文纵线格式)”或“ICCS坐标”表示的着法(在使用中文纵线格式记谱法时,可以有一定的字符容错,例如红车可以写成“车”而不必写成项目中的“俥”)。 每一行着法必须以阿拉伯数字+小数点作为起始,表示回合数,紧接着的两条着法记录之间以空格作为分隔符。 在文件中可以添加着法评注,评注必须写在一对花括号“{}”内,评注内可以是除花括号以外的任何字符,评注可以插在任何着法的后面,它和着法之间必须用分割符隔开; 可输出GIF,此功能需要预先安装前文提到的库。

由于各象棋软件产生的PGN文件规范不同,在“多枚相同棋子在同一条纵线上”时的着法表示可能与本程序不一致,此时需要手动对文件进行修改。

>>> board = cchess.Board.from_pgn("pgn/cg245.pgn", to_gif=True, gif_file="images/cg245.gif", duration=2)
100%|██████████| 27/27 [00:02<00:00, 11.32it/s]
GIF generated: 'images/cg245.gif'

>>> import cchess.svg
>>> board = cchess.Board.from_pgn("pgn/cg245.pgn")
>>> cchess.svg.to_html(board=board, filename="html/cg245.html", title="Chinese Chess Board")
HTML generated: 'html/cg245.html'
>>> board = cchess.Board.from_pgn("pgn/cg245.pgn", to_html=True, html_file="html/cg245.html")  # 或直接由PGN文件生成HTML棋谱。
HTML generated: 'html/cg245.html'

效果详见此示例

待补充…