一、引题

井字棋

井字棋,英文名叫Tic-Tac-Toe,是一种在3*3格子上进行的连珠游戏,和五子棋类似,由于棋盘一般不画边框,格线排成井字故得名。游戏需要的工具仅为纸和笔,然后由分别代表O和X的两个游戏者轮流在格子里留下标记(一般来说先手者为X),任意三个标记形成一条直线,则为获胜。

二、井字棋开发计划

第一阶段

程序维护井字棋棋盘,并要求两位人类玩家参与游戏。程序需在以下三种情况提升用户:
(1)输入格式不正确;(2)输入的坐标不在有效范围内;(3)落子位置不为空;
每走完一步,程序打印棋盘当前状态。

第二阶段

与第一阶段基本相同,增加提示玩家获胜的功能。

第三阶段

用计算机玩家代替一位人类玩家(计算机先走)。

第四阶段

人类玩家与计算机玩家对战,并可自由选择先后顺序。

三、计划实现

第一阶段

代码如下:

n=3
mat=[['.']*n for i in range(n)]
#主函数:通过调用函数get_move
#让两位人类玩家(X和O)交替地落子
#有效落子位置为(1,1)到(3,3)
def main():
    num_moves=0
    print_mat()
    print('Moves are r, c or "0" to exit.')
    exit_flag=False
    while not exit_flag:
        num_moves+=1
        if num_moves>9:
            print('No more space left')
            break
        player_ch='X' if num_moves%2>0 else 'O'
        exit_flag, r, c =get_move(player_ch)                         #r,c这一阶段未使用

#获取落子位置的函数
#不断让人类玩家(X或O)落子
#直到他以“行、列”方式指定了为空的位置
#然后将这个子加入棋盘再打印棋盘
def get_move(player_ch):
    while True:
        prompt='Enter move for '+ player_ch + ':'
        s=input(prompt)
        a_list=s.split(',')                                   #读取用户输入
        #错误输入
        if len(a_list)>=1 and int(a_list[0])==0:
            print('Bye now.')
            return True, 0, 0                                  #将退出标志设置为True
        elif len(a_list)<2:
            print('Use row,col. Re-enter.')
        #正确输入
        else:
            #首先,转换为从0开始的索引,有效落子位置转换为实际落子位置(0,0)到(2,2)
            r=int(a_list[0])-1                           #行
            c=int(a_list[1])-1                           #列
            if r<0 or r>=n or c<0 or c>=n:
                print('Out of range. Re-enter.')
            elif mat[r][c] != '.':
                print('Occupied square. Re-enter.')
            else:
                mat[r][c]=player_ch
                print_mat()
                break
    return False, r, c                                             #将退出标志设置为False

def print_mat():
    s=' 1 2 3\n'
    for i in range(n):
        s += str(i+1)+''
        for j in range(n):
            s += str(mat[i][j])+''
        s+='\n'
    print(s)
main()

第二阶段

这一阶段将为游戏增加一项重要的功能:玩家落子后检查他是否将3颗棋子连成线了。这一功能可用来为计算机玩家制定最优策略,而这正是我们在第三阶段要做的。
这一阶段的大多数代码与第一阶段相同,增加了一部分代码,代码如下:

n=3
mat=[['.']*n for i in range(n)]
#新增代码1(8种获胜组合)
win_list=[[1,2,3],[4,5,6],[7,8,9],
          [1,4,7],[2,5,8],[3,6,9],
          [1,5,9],[3,5,7]]
#主函数:通过调用函数get_move
#让两位人类玩家(X和O)交替地落子
#有效落子位置为(1,1)到(3,3)
def main():
    num_moves=0
    print_mat()
    print('Moves are r, c or "0" to exit.')
    exit_flag=False
    while not exit_flag:
        num_moves+=1
        if num_moves>9:
            print('No more space left')
            break
        player_ch='X' if num_moves%2>0 else 'O'
        exit_flag, r, c =get_move(player_ch)
        # 新增代码1 获胜提示
        if (not exit_flag) and test_win(r,c):
            print('\n', player_ch, 'Wins the game!' )
            break

#获取落子位置的函数
#不断让人类玩家(X或O)落子
#直到他以“行、列”方式指定了为空的位置
#然后将这个子加入棋盘再打印棋盘
def get_move(player_ch):
    while True:
        prompt='Enter move for '+ player_ch + ':'
        s=input(prompt)
        a_list=s.split(',')                                   #读取用户输入
        #错误输入
        if len(a_list)>=1 and int(a_list[0])==0:
            print('Bye now.')
            return True, 0, 0                                  #将退出标志设置为True
        elif len(a_list)<2:
            print('Use row,col. Re-enter.')
        #正确输入
        else:
            #首先,转换为从0开始的索引,有效落子位置转换为实际落子位置(0,0)到(2,2)
            r=int(a_list[0])-1                           #行
            c=int(a_list[1])-1                           #列
            if r<0 or r>=n or c<0 or c>=n:
                print('Out of range. Re-enter.')
            elif mat[r][c] != '.':
                print('Occupied square. Re-enter.')
            else:
                mat[r][c]=player_ch
                print_mat()
                break
    return False, r, c                                             #将退出标志设置为False

def print_mat():
    s=' 1 2 3\n'
    for i in range(n):
        s += str(i+1)+''
        for j in range(n):
            s += str(mat[i][j])+''
        s+='\n'

#新增代码1
#win_list是一个包含所有获胜组合的列表
#ttt_list为包含特定获胜组合的列表,如[1,2,3],my_win_list为包含所有这样的获胜组合的列表,即其中包含当前单元格的所有列表
#这个函数检查my_win_list中的所有组合
#只要有一个组合包含3个X或3个O,就返回True
#
def test_win(r,c):
    cell_n = r*3+c+1   #计算单元格编号(1-9)
    my_win_list=[ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_x == 3 or num_o == 3:
            return  True
    return False

def test_way(cell_list):
    letters_list=[]
    #创建一个形如['X','.','O']的列表
    for cell_n in cell_list:
        r = (cell_n-1) // 3
        c = (cell_n-1) % 3
        letters_list.append(mat[r][c])
    num_x =letters_list.count('X')                                #计算X的个数
    num_o =letters_list.count('O')                                #计算O的个数
    num_blanks =letters_list.count('.')
    return num_x, num_o, num_blanks

main()

第三阶段

在这一阶段我们需要一个规则(启发法)层次结构。(完全按照列出的顺序执行):
1.如果是第3步,且对手第2步下在边上,就下在中央(这是特殊规则)。
2.如果存在可立即获胜的位置,就下在这个位置。
3.如果存在可让对手立即获胜的位置,就下在这个位置。
4.如果有可形成双二(有两条线可以获胜)的位置,就下在这个位置。
5.否则,下在优先列表中第一个可下的位置。
代码如下:

n=3
mat=[['.']*n for i in range(n)]
#新增代码1(8种获胜组合)
win_list=[[1,2,3],[4,5,6],[7,8,9],
          [1,4,7],[2,5,8],[3,6,9],
          [1,5,9],[3,5,7]]
#主函数:通过调用函数get_move
#让两位玩家(X和O)交替地落子
#有效落子位置为(1,1)到(3,3)
def main():
    r = c =0                 #新增代码2
    num_moves=0
    print_mat()
    print('Moves are r, c or "0" to exit.')
    exit_flag=False
    while not exit_flag:
        num_moves+=1
        if num_moves>9:
            print('No more space left')
            break
        '''
        原代码
        player_ch='X' if num_moves%2>0 else 'O'
        exit_flag, r, c =get_move(player_ch)
        # 新增代码1 获胜提示
        if (not exit_flag) and test_win(r,c):
            print('\n', player_ch, 'Wins the game!' )
            break
        修改为:
        '''
        if num_moves %2>0:
            cell_n = 3*r+c+1
            r,c=get_comp_move(num_moves,cell_n)
            mat[r][c] = "X"
            print('\nOkey,my move...\n')
            print_mat()
            if test_win(r,c):
                print('\nX wins the game!')
                break
            else:
                exit_flag,r,c=get_move('O')
                if (not exit_flag) and test_win(r,c):
                    print('\nO wins the game!')
                    break

#获取落子位置的函数
#不断让人类玩家(X或O)落子
#直到他以“行、列”方式指定了为空的位置
#然后将这个子加入棋盘再打印棋盘
def get_move(player_ch):
    while True:
        prompt='Enter move for '+ player_ch + ':'
        s=input(prompt)
        a_list=s.split(',')                                   #读取用户输入
        #错误输入
        if len(a_list)>=1 and int(a_list[0])==0:
            print('Bye now.')
            return True, 0, 0                                  #将退出标志设置为True
        elif len(a_list)<2:
            print('Use row,col. Re-enter.')
        #正确输入
        else:
            #首先,转换为从0开始的索引,有效落子位置转换为实际落子位置(0,0)到(2,2)
            r=int(a_list[0])-1                           #行
            c=int(a_list[1])-1                           #列
            if r<0 or r>=n or c<0 or c>=n:
                print('Out of range. Re-enter.')
            elif mat[r][c] != '.':
                print('Occupied square. Re-enter.')
            else:
                mat[r][c]=player_ch
                print_mat()
                break
    return False, r, c                                             #将退出标志设置为False

def print_mat():
    s=' 1 2 3\n'
    for i in range(n):
        s += str(i+1)+''
        for j in range(n):
            s += str(mat[i][j])+''
        s+='\n'
    print(s)

#新增代码1
#win_list是一个包含所有获胜组合的列表
#ttt_list为包含特定获胜组合的列表,如[1,2,3],my_win_list为包含所有这样的获胜组合的列表,即其中包含当前单元格的所有列表
#这个函数检查my_win_list中的所有组合
#只要有一个组合包含3个X或3个O,就返回True
#
def test_win(r,c):
    cell_n = r*3+c+1   #计算单元格编号(1-9)
    my_win_list=[ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_x == 3 or num_o == 3:
            return  True
    return False

def test_way(cell_list):
    letters_list=[]
    #创建一个形如['X','.','O']的列表
    for cell_n in cell_list:
        r = (cell_n-1) // 3
        c = (cell_n-1) % 3
        letters_list.append(mat[r][c])
    num_x =letters_list.count('X')                                #计算X的个数
    num_o =letters_list.count('O')                                #计算O的个数
    num_blanks =letters_list.count('.')
    return num_x, num_o, num_blanks

#新增代码2(启发法设计计算机用户)
#确定计算机下一步要走什么地方的函数。
#对于棋盘上每个空单元格,检查它能否满足如下3个条件:
#1.让自己能立即获胜;2.让对手能立即获胜;3.让自己成双二(即下一步存在两种胜利条件)。
#如果这些条件都不满足,就根据优先列表选择。
def get_comp_move(num_moves, opp_cell):
    #如果这是第3步,且对手第二步下在边上,就下在中央。
    if num_moves == 3 and opp_cell in [2,4,6,8]:
        return 1,1                    #下在中央

    #生成一个包含所有空单元格的列表
    cell_list = [(i,j) for j in range(n) for i in range(n) if mat[i][j] =='.' ]

    #检查每个空单元格,看它能否让我方立即获胜
    for cell in cell_list:
        if test_to_win(cell[0],cell[1]):
            return cell[0],cell[1]

    #检查每个空单元格,看它能否让对手立即获胜
    for cell in cell_list:
        if test_to_block(cell[0],cell[1]):
            return cell[0],cell[1]

    #检查每个空单元格,看它能否成双二
    for cell in cell_list:
        if test_double_threat(cell[0], cell[1]):
            return cell[0], cell[1]
    pref_list=[1,9,3,7,5,2,4,6,8]               #创建优先列表,并从中选择第一个未占据的单元格。
    for i in pref_list:
        r=(i-1)//3
        c=(i-1)%3
        if mat[r][c]==".":
            return r,c

#检查能否获胜:检查包含当前单元格的每个获胜组合......
#如果其中包含两个X,就能立即获胜!
def test_to_win(r,c):
    cell_n = r*3+c+1
    my_win_list=[ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x,num_o,num_blanks=test_way(ttt_list)
        if num_x ==2:
            print('Watch this...')
            return True
    return False

#检查能否让对手获胜:检查包含当前单元格的每个获胜组合......
#如果其中包含两个O,就能立即让对手获胜!
def test_to_block(r,c):
    cell_n=r*3+c+1
    my_win_list = [ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_o == 2:
            print('Ha ha, I am going to block you!')
            return True
    return False

#检查能否成双二:检查包含当前单元格的所有获胜组合
#如果有两个组合都有X,就能成双二。
def test_double_threat(r,c):
    threats=0
    cell_n = r * 3 + c + 1
    my_win_list = [ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_x == 1 and num_blanks == 2:
            threats+=1
        if threats>=2:
            print('I have you now!')
            return True
    return False

main()

第四阶段

这一阶段增加了计算机后手以及玩家自由选择先后顺序的功能,在一定程度上确保了游戏的完整性。
代码如下:

n=3
mat=[['.']*n for i in range(n)]
#新增代码1(8种获胜组合)
win_list=[[1,2,3],[4,5,6],[7,8,9],
          [1,4,7],[2,5,8],[3,6,9],
          [1,5,9],[3,5,7]]
#主函数:通过调用函数get_move,让两位玩家(X和O)交替地落子
#有效落子位置为(1,1)到(3,3)
def main2():                                                                #玩家先走
    pref_list = [5, 1, 9, 3, 7, 2, 4, 6, 8]  # 创建优先列表,并从中选择第一个未占据的单元格。(玩家先走)
    num_moves=0
    print_mat()
    print('Moves are r, c or "0" to exit.')
    exit_flag=False
    while not exit_flag:
        num_moves+=1
        if num_moves>9:
            print('No more space left')
            break
        
        if num_moves %2>0:
            exit_flag, r, c = get_move('O')
            print_mat()
            if test_win(r,c):
                print('\nO wins the game!')
                break
        else:
            opp_cell = 3*r + c + 1
            machine_turn(num_moves,opp_cell)
def main1():                                                                #计算机先走
    pref_list = [1, 9, 3, 7, 5, 2, 4, 6, 8]  # 创建优先列表,并从中选择第一个未占据的单元格。(计算机先走)
    r = c =0
    num_moves=0
    print_mat()
    print('Moves are r, c or "0" to exit.')
    exit_flag=False
    while not exit_flag:
        num_moves+=1
        if num_moves>9:
            print('No more space left')
            break
        if num_moves %2>0:
            cell_n = 3*r+c+1
            r,c=get_comp_move1(num_moves,cell_n)
            mat[r][c] = "X"
            print('\nOkey,my move...\n')
            print_mat()
            if test_win(r,c):
                print('\nX wins the game!')
                break
            else:
                exit_flag,r,c=get_move('O')
                if (not exit_flag) and test_win(r,c):
                    print('\nO wins the game!')
                    break

#获取落子位置的函数
#不断让玩家(X或O)落子
#直到他以“行、列”方式指定了为空的位置
#然后将这个子加入棋盘再打印棋盘
def get_move(player_ch):
    while True:
        prompt='Enter move for '+ player_ch + ':'
        s=input(prompt)
        a_list=s.split(',')                                   #读取用户输入
        #错误输入
        if len(a_list)>=1 and int(a_list[0])==0:
            print('Bye now.')
            return True, 0, 0                                  #将退出标志设置为True
        elif len(a_list)<2:
            print('Use row,col. Re-enter.')
        #正确输入
        else:
            #首先,转换为从0开始的索引,有效落子位置转换为实际落子位置(0,0)到(2,2)
            r=int(a_list[0])-1                           #行
            c=int(a_list[1])-1                           #列
            if r<0 or r>=n or c<0 or c>=n:
                print('Out of range. Re-enter.')
            elif mat[r][c] != '.':
                print('Occupied square. Re-enter.')
            else:
                mat[r][c]=player_ch
                print_mat()
                break
    return False, r, c                                             #将退出标志设置为False

def print_mat():
    s=' 1 2 3\n'
    for i in range(n):
        s += str(i+1)+''
        for j in range(n):
            s += str(mat[i][j])+''
        s+='\n'
    print(s)

#新增代码1
#win_list是一个包含所有获胜组合的列表
#ttt_list为包含特定获胜组合的列表,如[1,2,3],my_win_list为包含所有这样的获胜组合的列表,即其中包含当前单元格的所有列表
#这个函数检查my_win_list中的所有组合
#只要有一个组合包含3个X或3个O,就返回True
#
def test_win(r,c):
    cell_n = r*3+c+1   #计算单元格编号(1-9)
    my_win_list=[ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_x == 3 or num_o == 3:
            return  True
    return False

def test_way(cell_list):
    letters_list=[]
    #创建一个形如['X','.','O']的列表
    for cell_n in cell_list:
        r = (cell_n-1) // 3
        c = (cell_n-1) % 3
        letters_list.append(mat[r][c])
    num_x =letters_list.count('X')                                #计算X的个数
    num_o =letters_list.count('O')                                #计算O的个数
    num_blanks =letters_list.count('.')
    return num_x, num_o, num_blanks

#新增代码2(启发法设计计算机用户)
#确定计算机下一步要走什么地方的函数。
#对于棋盘上每个空单元格,检查它能否满足如下3个条件:
#1.让自己能立即获胜;2.让对手能立即获胜;3.让自己成双二(即下一步存在两种胜利条件)。
#如果这些条件都不满足,就根据优先列表选择。
def get_comp_move1(num_moves,opp_cell):                                         #计算机先走
    #如果这是第3步,且对手第二步下在边上,就下在中央。
    if num_moves == 3 and opp_cell in [2,4,6,8]:
        return 1,1                    #下在中央

    #生成一个包含所有空单元格的列表
    cell_list = [(i,j) for j in range(n) for i in range(n) if mat[i][j] =='.' ]

    #检查每个空单元格,看它能否让我方立即获胜
    for cell in cell_list:
        if test_to_win(cell[0],cell[1]):
            return cell[0],cell[1]

    #检查每个空单元格,看它能否让对手立即获胜
    for cell in cell_list:
        if test_to_block(cell[0],cell[1]):
            return cell[0],cell[1]

    #检查每个空单元格,看它能否成双二
    for cell in cell_list:
        if test_double_threat(cell[0], cell[1]):
            return cell[0], cell[1]

    pref_list = [1, 9, 3, 7, 5, 2, 4, 6, 8]  # 创建优先列表,并从中选择第一个未占据的单元格。(计算机先走)
    for i in pref_list:
        r=(i-1)//3
        c=(i-1)%3
        if mat[r][c]==".":
            return r,c

def get_comp_move2(num_moves,opp_cell):                                   #玩家先走
    #第四步陷阱
    if num_moves == 4 :
        if (mat[0][0]=='O' and mat[2][2]=='O') or (mat[0][2]=='O' and mat[2][0]=='O'):
            return 0,1
    #生成一个包含所有空单元格的列表
    cell_list = [(i,j) for j in range(n) for i in range(n) if mat[i][j] =='.' ]

    #检查每个空单元格,看它能否让我方立即获胜
    for cell in cell_list:
        if test_to_win(cell[0],cell[1]):
            return cell[0],cell[1]

    #检查每个空单元格,看它能否让对手立即获胜
    for cell in cell_list:
        if test_to_block(cell[0],cell[1]):
            return cell[0],cell[1]

    #检查每个空单元格,看它能否成双二
    for cell in cell_list:
        if test_double_threat(cell[0], cell[1]):
            return cell[0], cell[1]

    pref_list = [5, 1, 9, 3, 7, 2, 4, 6, 8]  # 创建优先列表,并从中选择第一个未占据的单元格。(玩家先走)
    for i in pref_list:
        r=(i-1)//3
        c=(i-1)%3
        if mat[r][c]==".":
            return r,c

#检查能否获胜:检查包含当前单元格的每个获胜组合......
#如果其中包含两个X,就能立即获胜!
def test_to_win(r,c):
    cell_n = r*3+c+1
    my_win_list=[ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x,num_o,num_blanks=test_way(ttt_list)
        if num_x ==2:
            print('Watch this...')
            return True
    return False

#检查能否让对手获胜:检查包含当前单元格的每个获胜组合......
#如果其中包含两个O,就能立即让对手获胜!
def test_to_block(r,c):
    cell_n=r*3+c+1
    my_win_list = [ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_o == 2:
            print('Ha ha, I am going to block you!')
            return True
    return False

#检查能否成双二:检查包含当前单元格的所有获胜组合
#如果有两个组合都有X,就能成双二。
def test_double_threat(r,c):
    threats=0
    cell_n = r * 3 + c + 1
    my_win_list = [ttt_list for ttt_list in win_list if cell_n in ttt_list]
    for ttt_list in my_win_list:
        num_x, num_o, num_blanks = test_way(ttt_list)
        if num_x == 1 and num_blanks == 2:
            threats+=1
        if threats>=2:
            print('I have you now!')
            return True
    return False

def machine_turn(num_moves,opp_cell):
    r, c = get_comp_move2(num_moves,opp_cell)
    mat[r][c] = "X"
    print('\nOkey,my move...\n')
    print_mat()
    if test_win(r, c):
        return print('\nX wins the game!')

a=input("Choose to go first(0 is you go first. 1 is the machine. ):")
if a=='0':
    main2()
elif a=='1':
    main1()

四、关于启发法

要玩好游戏,计算机必须采用特定的战略和战术。这被称为启发法,这大致相当于做判断。
从很大程度上说,启发法可归结为来自主要的方法:向前搜索根据价值做出选择
(向前搜索有时称为暴力算法