Python 实现中国象棋 Fen 串棋谱转gif 动态图片

Python 实现中国象棋 Fen 串棋谱转gif 动态图片

在录棋谱时候,常有把棋谱转为gif动态图片的需要,第三方工具使用得不顺手,自己动手弄了一个吧。

一、UCCI协议

中国象棋的棋谱,目前没有形成一个统一的标准,常见的有东萍棋谱网的棋谱和UCCI (Universal Chinese Chess Protocol,中国象棋通用引擎协议)格式棋谱,还有就是各大游戏平台,各家都撸了一套自己的格式,反正就是没有一个统一的标准。

这里实现的是UCCI协议格式的棋谱转GIF图片文件。棋谱协议参考:https://www.xqbase.com/protocol/cchess_ucci.htm棋谱格式为类似如下,名局《七星聚会》棋谱(主分支):

4rk3/3P5/4bP3/9/9/8P/9/1p2p2C1/3p1p3/4K1RR1 w moves h2f2 e2f2 f7f8 f9f8 g0g8 f8f9 h0h1 f1e1 h1e1 d1e1 e0e1 f2f1 e1e2 e9c9 d8c8 c9a9 g8g7 f9f8 g7g8 f8f9 g8g6 a9a2 g6f6 f9e9 f6f1 b2c2 f1d1 c2c1 d1d2 a2a4 d2d7 a4e4 e2d2 e4e0 d7d9 e9e8 d9h9 e0a0 d2e2 c1d1 h9d9 a0i0 d9f9 i0i2 f9f2 i2i4 f2f9 i4e4 e2d2 e4d4 d2e2 d4d3 f9h9 d3c3 h9d9 d1c1 d9h9 c1d1 h9d9 e8f8 d9d6 c3f3 e2d2 d1c1 d2e2 c1d1 e2d2 d1e1 d6e6 e1f1 e6h6 f3d3 d2e2 d3c3 e2d2 e7g5 c8d8 f8f7 h6g6 g5i7 g6g7 f7f8 g7i7 c3g3 i7i5

前面部分“4rk3/3P5/4bP3/9/9/8P/9/1p2p2C1/3p1p3/4K1RR1 w” 为棋谱初始局面,w表示轮到红,如果b表示轮到黑,moves 之后四个字符为一组,空格分开,每组为每一招。

简单解析下moves格式,对应棋盘横向坐标由左到右为:a-i,纵向由下向上为:0-9,如下图:

如《七星聚会》第一招h2f2如上所示,结合初始棋谱信息,也就是炮二平四了。

二、棋盘数据设计

这里使用16*16数组表示棋盘信息,而棋谱真实信息是8*9,会冗余部分数据,棋子位置信息大概如下:

为什么这样设计?后续有机会在其他篇章再介绍了。

定义棋子数据和坐标信息:

RANK_TOP = 3
RANK_BOTTOM = 12
RANK_LEFT = 3

SQUARE_SIZE = 256

PIECE_RED   = 0x08
PIECE_BLACK = 0x10

PIECE_KING    = 0x01
PIECE_ADVISOR = 0x02
PIECE_BISHOP  = 0x03
PIECE_KNIGHT  = 0x04
PIECE_ROOK    = 0x05
PIECE_CANNON  = 0x06
PIECE_PAWN    = 0x07

stypes = {
    "background":"./res/bg.png", # 棋盘图片
    "pieces":"./res/pieces.png", # 棋子图片
    "width":555,          # 画布宽度
    "height":618,         # 画布高度
    "cellSize":57,        # 格子大小
    "pieceSize":58,       # 棋子大小
    "startX":51,          # 第一个着点X坐标;
    "startY":54,          # 第一个着点Y坐标;
}


# 转Y坐标
def RANK_Y(sq):
    return sq >> 4

# 转X坐标
def RANK_X(sq):
    return sq & 15

# 转数据棋盘坐标
def COORD_XY(x, y):
    return x + (y << 4)

三、协议解析

fen 转棋谱初始局面数据的代码:

def squaresfromFen(fen):
    squares = [ 0 for i in range(SQUARE_SIZE)]

    x = RANK_LEFT
    y = RANK_TOP

    for c in fen:
        if c == "/":
            x = RANK_LEFT
            y += 1

            if y > RANK_BOTTOM:
                break
        elif "1" <= c and c <= "9":
            x += int(c)
        elif (("A" <= c and c <= "Z") or ("a" <= c and c <= "z")):
            squares[COORD_XY(x, y)] = CHAR_TO_PIECE(c);
            x += 1

    return squares

定义每个棋子信息后,解析fen棋子信息:

def CHAR_TO_PIECE(c):
    side = 0
    if ("A" <= c and c <= "Z"):
        side = PIECE_RED
    elif ("a" <= c and c <= "z"):
        side = PIECE_BLACK

    if c == "K" or c == "k":
        return PIECE_KING | side
    if c == "A" or c == "a":
        return PIECE_ADVISOR | side
    if c == "B" or c == "E" or c == "b" or c == "e":
        return PIECE_BISHOP | side
    if c == "H" or c == "N" or c == "h" or c == "n":
        return PIECE_KNIGHT | side
    if c == "R" or c == "r":
        return PIECE_ROOK | side
    if c == "C" or c == "c":
        return PIECE_CANNON | side
    if c == "P" or c =="p":
        return PIECE_PAWN | side

    return -1

解析棋谱招法信息,也就是将类似“h2f2”的信息解析为棋盘位置,每个招法四个字符,前面两个表示源,后面两表示目的地,也就是每两位可以解析为一个坐标位置:

def posStrToPos(s):
    x = "abcdefghi".find(s[0])
    y = "9876543210".find(s[1])
    if x == -1 or y == -1:
        return 0

    return COORD_XY(x+RANK_LEFT, y+RANK_TOP)

每个招法,简单用个元组表示(src,dst),也就是从哪到哪,招法的棋谱解析如下:


def moveStrToMove(s):
    src, dst = 0, 0
    if len(s) == 4:
        src = posStrToPos(s[:2])
        dst = posStrToPos(s[2:])

    return (src, dst)

棋盘一个状态,加一个move 招法,变到另一个状态,逻辑较为简单,就是把目标位置的数据改为源位置数据,再将源位置数据改为0,代码:

def MOVE(squares, src, to):
    squares[to] = squares[src]
    squares[src] = 0

四、画图部分

好了,棋谱信息解析的差不多了,下面是画图部分。

先准备两个图片,一个是棋盘,一个是棋子。

每一个招法后的棋盘状态,绘制一个图片,再把这些图片序列收集起来压成一个gif 便可。这里还是使用PIL库进行图片处理,这里不过多介绍PIL使用了。

棋谱数据转图片:

def toImage(squares):
    img_bg = Image.open(stypes['background'])
    img_pieces = Image.open(stypes['pieces'])
    img_pieces = img_pieces.convert('RGBA')

    img_result = img_bg.convert('RGBA')

    _pieceSize = stypes['pieceSize']

    for x in range(9):
        for y in range(10):
            i = COORD_XY(x+RANK_LEFT, y+RANK_TOP)

            piece = squares[i]
            if piece == 0:
                continue

            cropX, cropY= 0, 0
            if (piece & PIECE_BLACK):
                cropY = _pieceSize

            cropX = ((piece & 0x07) - 1) * _pieceSize

            img_piece = img_pieces.crop((cropX, cropY, cropX + _pieceSize, cropY + _pieceSize))

            pastePosX = stypes['startX'] - int(_pieceSize/2) + x * _pieceSize
            pastePosY = stypes['startY'] - int(_pieceSize/2) + y * _pieceSize

            img_result.paste(img_piece, (pastePosX, pastePosY), img_piece)、

    return img_result

对square绘制出来的棋谱状态(《七星聚会》初始状态):

最后一步,处理每一招的后的棋盘状态信息,把它们保持为一个gif 文件:

def make(fen, moves, save_path):
    squares = squaresfromFen(fen)
    images = [toImage(squares)]

    for (src, dst) in moves:
        MOVE(squares, src, dst)
        images.append(toImage(squares))

    images[0].save(save_path, save_all=True, append_images=images[1:], duration=1500, loop=0)

把这些代码组合起来就是完整的实现。

输入一个fen 串,就可以得到一个gif 棋谱了,如前面《七星聚会》的fen棋谱串,转为gif动态图如下:

把这些代码组合起来就是完整的实现。输入一个fen 串,就可以得到一个gif 棋谱了,如前面《七星聚会》的fen棋谱串,转为gif动态图如下:

最后,再结合自己的需要,做水印或者图片大小压缩或者尺寸压缩,PIL 有挺丰富的接口,简单的使用都可以组合出很多丰富的功能。

如果自己定制棋盘和棋子,换图就可,也非常方便

(全文完)

(欢迎转载本站文章,但请注明作者和出处 云域 – Yuccn

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注