目录

1. 前言

2. 算法流程

3. 代码实现

3.1 终局及胜负判定方法

3.2 搜索邻节点

3.3 打印棋盘状态

3.4 代码

4. 小结


1. 前言

        Tic-Tac-Toe中文常译作井字棋,即在3 x 3的棋盘上,双方轮流落子,先将3枚棋子连成一线的一方获得胜利。Tic-Tac-Toe变化简单,可能的局面和棋局数(注意区分局面与棋局!)都很有限(相比中国象棋、日本象棋、围棋等来说连九牛一毛都不到!具体有多少可能的局面以及可能的棋局数,本系列完成以后就可以给出答案了),因此常成为博弈论游戏树搜寻的教学例子,同时也是人工智能的一道好题目。

        本系列考虑实现一个Tic-Tac-Toe AI,以由浅入深循序渐进的方式来逐步完成这个实现。途中将会介绍minimax、alpha-beta pruning、MCTS等等算法,最终实现一个不可战胜的Tic-Tac-Toe AI,并以此为基础考虑更复杂的博弈游戏AI的实现。

        Tic-Tac-Toe棋盘为3x3的正方形,共9个格子(tile or grid)。棋盘状态可以用一个含9个元素的一维数组表示,或者一个3x3的二维数组表示。这里假定用9个元素的一维数组board[9]表示,其中[0],[1],[2]表示棋盘的最上一行从左到右三个格子;[3],[4],[5]表示棋盘的中间一行从左到右三个格子;[6],[7],[8]表示棋盘的中间一行从左到右三个格子。

        每个格子有三种状态,分别用0,1,2表示如下:

        0: empty tile

        1: X tile,假定总是X先走。

        2: O tile

        考虑以树的形式来表达所有可能的游戏过程(或者说棋局)。其中每个盘面状态表示一个节点,树的根节点就是棋盘的初始状态。一个游戏过程(或者说棋局)表示一局可能的Tic-Tac-Toe游戏的按顺序构成的盘面状态序列。比如说,第1个状态肯定是初始状态;第2个状态是由player1下了一手棋后所到达的状态;第3个状态是由player2下了一手棋后所到达的状态;。。。以下以此类推。

       本文首先考虑搜索一种可能的游戏过程,也就是从树的根节点到达某个可能的终局状态的路径。这个可以用常见的深度优先搜索或者广度优先搜索的方式来实现。

        初始状态显然为s0 = [0,0,0, 0,0,0, 0,0,0].

        终局可以分为以下几种可能:

  1. 决出胜负的局面(双方总共手数可能等于9手也可能小于9手)
  2. 平局(双方手数必然等于9手)

2. 算法流程

        用深度(或广度)优先算法解决这一搜索问题的伪代码描述如下:

        Visit用python set实现,这样可以利用python set的高效的查询效率。考虑到python set的元素必须是hashable类型,所以棋盘状态用tuple表示。 

3. 代码实现

3.1 终局及胜负判定方法

        参见is_endofgame()

        判定当前局面是否终局。当前用了个比较呆笨的办法。用win_comb预存了所有可能的胜利局面,然后针对当前局面,遍历是否某一个玩家的棋子满足win_comb中的某个组合。注意,由于在搜索过程中,是按照每人一手交替的方式前进的,不可能存在两个棋手的棋子都满足胜局条件的情况。

        本函数给出的结果包括:是否终局;如果是的话,则进一步给出胜负结果(是否平局,不是的话赢家是谁)。

3.2 搜索邻节点

        参见find_neighbor()

        针对当前棋盘状态,给出可能的下一个棋盘状态的列表。

        首先根据当前棋盘状态确定接下来该谁下(whose turn),然后遍历所有空的棋盘格(tile or grid)给出所有可能的下一个棋盘状态的列表。

        

3.3 打印棋盘状态

        参见print_board()

        将用一维数组表示的棋盘状态用二维的方式描绘出来以方便查看。

        注意,如前所述,总是假定“X”先走。一个可能的终局状态如下所示:

Tic-Tac-Toe可能棋局搜索的实现(python)-LMLPHP

 

3.4 代码

 

# -*- coding: utf-8 -*-
"""
Created on Sat Dec 31 12:53:10 2022

@author: chenxy
"""

import random
from collections import deque

win_comb=((0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6))

def is_endofgame(s):
    end_flag = False
    winner   = 0 # draw or tie.
    
    for comb in win_comb:
        if s[comb[0]]==1 and s[comb[1]]==1 and s[comb[2]]==1:
            winner = 1
            end_flag = True
            break
        elif s[comb[0]]==2 and s[comb[1]]==2 and s[comb[2]]==2:
            winner = 2
            end_flag = True
            break
        else:
            continue
    
    if (not end_flag) and s.count(0)==0:
        end_flag = True
            
    return end_flag, winner        

def find_neighbor(s):
    neighbor_list = []
    # decides whose turn
    if s.count(1) == s.count(2):
        turn = 1
    elif s.count(1) == s.count(2) + 1:
        turn = 2
    else:
        print('Invalid input state: ', s)
        return None
    
    for k in range(len(s)):
        if s[k] == 0:
            s_next = list(s)
            s_next[k] = turn
            neighbor_list.append(tuple(s_next))
    return neighbor_list[::-1]

def print_board(s):
    print('----------')
    for k in range(len(s)):
        if k%3 == 2:
            end = ' \n----------\n'        
        else:
            end = ' | '
        if s[k] == 1: 
            char = "X"
        elif s[k] == 2: 
            char = "O"
        else:
            char=' '
        print(char,end=end)

# Initialization
s0   = tuple([0] * 9)
path = []
q    = deque()
visited = set()

# Put initial state s0 into stack/queue
q.append(s0)
visited.add(s0)

while 1:
    s = q.pop() # DFS: Depth First Search
    # s = q.popleft() # BFS: Breadth First Search
    path.append(s)
    print(s)
    end_flag, winner = is_endofgame(s)
    if end_flag:
        s_end = s 
        break
    
    neighbors = find_neighbor(s)
    # random.shuffle(neighbors)
    for neighbor in neighbors:
        if neighbor not in visited:
            q.append(neighbor)
            visited.add(neighbor)

for s in path:
    print_board(s)        

        运行结果如下: 

4. 小结

        以上实现只是沿着节点树的某一条确定性的分支走到底。由于只是寻找某(任意)一种游戏进行过程的路径,沿着任何一条分支都可以到达终局状态(可能分胜负,也可能是平局)。所以以上程序搜索得到的结果非常平凡。

        如何让它变得更有趣一些呢?追加一点随机性。如下所示追加将邻节点列表随机打乱顺序的处理:

Tic-Tac-Toe可能棋局搜索的实现(python)-LMLPHP

        这样就可以在每次运行时得到一个不同的棋局。

        下一篇将考虑Tic-Tac-Toe(3x3)所有可能的游戏过程,也即所有可能的(从初始状态到达终局的)路径。并由此可以统计出所有可能的盘面状态总数。

 

01-01 08:29