五子棋棋型精确检测

  • 1. 参考资料中的检测方法
  • 基本棋型
  • 2. 棋型精确检测的实现
  • 2.1 读取棋盘上的所有直线
  • 2.2 将每一条直线分段
  • 2.3 检测识别每一段的棋型
  • 3. 检测结果验证
  • 4. 参考资料:


1. 参考资料中的检测方法

基本棋型

参考:http://game.onegreen.net/wzq/HTML/142336.html
最常见的基本棋型大体有以下几种:连五,活四,冲四,活三,眠三,活二,眠二。
具体参看链接,或者网上搜索即可,这不是本文的重点。

参考资料中的检测方法,是遍历棋盘上的每一个位置,如果是需要检测方(AI方)的棋的话,即从四个方向:(1, 0)、(0, 1)、(1, 1)、(1, -1)即横、纵、左下斜、左上斜,分别检测直线上向前4颗和向后4颗棋,4颗是可以连成五子棋的边界,通过检测这9颗棋的归属来判断棋型。

该方法过于复杂,晦涩难懂,执行效率较低,代码也不美观,看得人只想…特别是检测模型不准确,在小棋上检测错误较多,大棋基本正确,但由于作者水平较高,后续连续升级了算法,最终AI取得较高的水准,是用降维打击的办法。本文远没有达到资料作者的高度,只是就其中棋型检测部分给出一个更优化的解决方法,还处在非常基础的层面,但做好基础工作依然有着重要的意义。

2. 棋型精确检测的实现

本文给出的五子棋棋型检测方法是:对于15 * 15的棋盘来说,纵横各有15条直线,加上左下斜和左上斜两个方向可能容纳5颗棋子以上的各21条线,共有72条直线,首先从棋盘上取出这72条,然后对每条直线上的棋子进行初步统计,本方(AI方)少于2颗棋子的行直接剔除,再按对手棋子位置对直线进行截取,再剔除长度小于5的片断,将所有有效片断(只有本方棋子和空位)集中于列表中,最后对每个片断进行检测,具体检测方法是将片断转换成字符串,然后利用Python中功能强大的re模块进行正则匹配检测,得到精确的检测数据。

棋盘board是一个二维列表,每个元素内容是一个整数,用0表示空位,1表示选手一的棋子(黑棋),2表示选手二的棋子(白棋)。

2.1 读取棋盘上的所有直线

# 取棋盘上的每行
    for y in range(LINES):
        lines.append(board[y])
        
    # 取棋盘上的每列
    for x in range(LINES):
        column = []
        for y in range(LINES):
            column.append(board[y][x])
        lines.append(column)
        
    # 取棋盘上的左向上的前11行,再取后10行
    for y in range(4, LINES):
        left_up = []
        for i in range(y+1):
            left_up.append(board[y-i][i])
        lines.append(left_up)
    for x in range(LINES-5):
        left_up = []
        for j in range(LINES-1, x, -1):
            left_up.append(board[j][x+LINES-j])
        lines.append(left_up)
        
    # 取棋盘上的左向下的前11行,再取后10行
    for y in range(LINES - 4):
        left_down = []
        for i in range(LINES-y):
            left_down.append(board[y+i][i])
        lines.append(left_down)
    for x in range(LINES - 5):
        left_down = []
        for j in range(LINES-x-1):
            left_down.append(board[j][x+j+1])
        lines.append(left_down)

2.2 将每一条直线分段

mine和opponent分别代表本方和对手,分别为整数1或2,mine在前表示正在对mine的棋型进行检测。

def intercept_line(line, mine, opponent):  # 将一行分成只有本方棋子与空位的小段
        my_num = line.count(mine)
        if my_num <= 1:  # 本方棋数量少于1个则结束检测
            return
            
        pieces = []
        op_num = line.count(opponent)  # 对方的棋数
        if op_num > 0:
            op_index = []  # 记录对方的棋所在的位置/index,将本行截成多个小段
            for j in range(len(line)):
                if line[j] == opponent:
                    op_index.append(j)
            assert op_num == len(op_index), '对手棋子的"统计结果"与"逐个核查结果"必须相符!'
            # 在op_index列表的头尾分别插入-1和len(line),以方便下述截取小段
            op_index.insert(0, -1)
            op_index.append(len(line))
            for idx in range(1, len(op_index)):
                chess_range = op_index[idx] - op_index[idx - 1] - 1  # 不算两端的对手的棋
                if chess_range >= 5:
                    # 截出更小长度的段以供检测
                    tmp_piece = line[op_index[idx - 1] + 1: op_index[idx]]
                    if tmp_piece.count(mine) > 1:
                        pieces.append(tmp_piece)
        else:
            pieces.append(line)
            
        return pieces

2.3 检测识别每一段的棋型

S2, L2, S3, L3, S4, L4, L5 = 0, 1, 2, 3, 4, 5, 6
SCORE_LIST = [2, 8, 10, 100, 1000, 10000, 100000]
    
    def detect_piece(piece, mine):
        # 先将piece转换成字串,使用正则表达式来匹配棋型
        string = ''
        for chess in piece:
            if chess == 0:
                string += 'O'
            else:
                string += 'X'
                
        # 按棋型得分和棋子相连的多少降序检测
        if re.search(r'XXXXX', string):
            count[L5] += 1
        elif re.search(r'OXXXXO', string):
            count[L4] += 1
        elif re.search(r'XOXXXOX', string):
            count[L4] += 1
        elif re.search(r'XXOXXOXX', string):
            count[L4] += 1
        elif re.search(r'^XXXXO', string):
            count[S4] += 1
        elif re.search(r'OXXXX$', string):
            count[S4] += 1
        elif re.search(r'XOXXX', string):
            count[S4] += 1
        elif re.search(r'XXXOX', string):
            count[S4] += 1
        elif re.search(r'XXOXX', string):
            count[S4] += 1
        elif re.search(r'OOXXXO', string):
            count[L3] += 1
        elif re.search(r'OXXXOO', string):
            count[L3] += 1
        elif re.search(r'OXXOXO', string):
            count[L3] += 1
        elif re.search(r'OXOXXO', string):
            count[L3] += 1
        elif re.search(r'XOXOXOX', string):
            count[L3] += 1
        elif re.search(r'^OXXXO$', string):
            count[S3] += 1
        elif re.search(r'^XXXOO', string):
            count[S3] += 1
        elif re.search(r'OOXXX$', string):
            count[S3] += 1
        elif re.search(r'^XXOXO', string):
            count[S3] += 1
        elif re.search(r'OXOXX$', string):
            count[S3] += 1
        elif re.search(r'^XOXXO', string):
            count[S3] += 1
        elif re.search(r'OXXOX$', string):
            count[S3] += 1
        elif re.search(r'XOOXXOOX', string):
            count[S3] += 2
        elif re.search(r'XOOXX', string):
            count[S3] += 1
        elif re.search(r'XXOOX', string):
            count[S3] += 1
        elif re.search(r'XOXOX', string):
            count[S3] += 1
        elif re.search(r'OOXXOO', string):
            count[L2] += 1
        elif re.search(r'OXOXOO', string):
            count[L2] += 1
        elif re.search(r'OOXOXO', string):
            count[L2] += 1
        elif re.search(r'OXOOXO', string):
            count[L2] += 1
        elif re.search(r'XOOOX', string):
            count[S2] += 1
        elif re.search(r'^XOOXO', string):
            count[S2] += 1
        elif re.search(r'OXOOX$', string):
            count[S2] += 1
        elif re.search(r'^XOXOO', string):
            count[S2] += 1
        elif re.search(r'OOXOX$', string):
            count[S2] += 1

        if re.search(r'^XXOOO', string):
            count[S2] += 1
        elif re.search(r'^OXXOO$', string):
            count[S2] += 1
        elif re.search(r'^OOXXO$', string):
            count[S2] += 1
        elif re.search(r'OOOXX$', string):
            count[S2] += 1

3. 检测结果验证

配合一小段代码,对上述检测五子棋棋型的方法进行了验证,结果如下:
测试代码:

if __name__ == '__main__':
    AI = WBEasyAI()
    lines = [[1, 0, 0, 1, 0, 2, 0, 1, 0, 1, 1, 0, 2, 0, 0],
             [2, 0, 2, 1, 2, 2, 1, 1, 0, 1, 0, 2, 1, 1],
             [1, 0, 0, 1, 0, 2, 0, 1, 0, 1, 1, 0, 2, 0, 0],
             [0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 1, 1, 1],
             [2, 0, 1, 0, 1, 0, 0, 0],
             [2, 2, 0, 1, 1, 0, 1, 0, 1],
             [2, 1, 1, 1, 0, 1, 1, 1, 2],
             [2, 0, 1, 1, 2, 1, 1, 0, 0, 1, 1],
             [0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0],
             [1, 2, 1, 2, 0, 2, 0, 1, 0, 0, 1, 0, 1, 0]]
    for i in range(len(lines)):
        # line = AI.create_line()
        # lines.append(line)
        AI.intercept_line(lines[i], mine, opponent)
        print(lines[i])

    print(AI.pieces)

    for piece in AI.pieces:
        AI.detect_piece(piece, mine)

    print(AI.count)

测试结果:

pygame 2.0.1 (SDL 2.0.14, Python 3.9.0)
Hello from the pygame community. https://www.pygame.org/contribute.html
[1, 0, 0, 1, 0, 2, 0, 1, 0, 1, 1, 0, 2, 0, 0]
[2, 0, 2, 1, 2, 2, 1, 1, 0, 1, 0, 2, 1, 1]
[1, 0, 0, 1, 0, 2, 0, 1, 0, 1, 1, 0, 2, 0, 0]
[0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 1, 1, 1]
[2, 0, 1, 0, 1, 0, 0, 0]
[2, 2, 0, 1, 1, 0, 1, 0, 1]
[2, 1, 1, 1, 0, 1, 1, 1, 2]
[2, 0, 1, 1, 2, 1, 1, 0, 0, 1, 1]
[0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0]
[1, 2, 1, 2, 0, 2, 0, 1, 0, 0, 1, 0, 1, 0]

[[1, 0, 0, 1, 0], [0, 1, 0, 1, 1, 0], [1, 1, 0, 1, 0], [1, 0, 0, 1, 0], [0, 1, 0, 1, 1, 0], [0, 0, 1, 1, 1], [0, 1, 0, 1, 0, 0, 0], [0, 1, 1, 0, 1, 0, 1], [1, 1, 1, 0, 1, 1, 1], [1, 1, 0, 0, 1, 1], [0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0], [0, 1, 0, 0, 1, 0, 1, 0]]

[[2, 3, 3, 3, 2, 0, 0], [0, 0, 0, 0, 0, 0, 0]]

结果第一部分是随机生成的待检测棋面数据,1为需要检测的棋子,2是对手的棋,0为空位;
第二部分是截取出来的有效片断
最后一行是棋型统计结果,只看列表的第一个元素(第二个是对手的棋型统计,测试时未做统计),index 0~6,分别对应眠2、活2、眠3、活3、冲4、活4、连5,统计出的数量分别是2, 3, 3, 3, 2, 0, 0。结果准确。