当前位置:   article > 正文

人机对弈黑白棋游戏用Python tkinter的Canvas实现图形界面_canvas 黑白棋

canvas 黑白棋

在“Python游戏编程快速上手”一书中,给出一个字符界面的人机对弈黑白棋游戏。网友“孤舟钓客”用pyGame将这个字符界面黑白棋游戏改为图形界面,计算机落子采用的算法及算法程序和字符界面游戏基本相同。该文网址是:https://blog.csdn.net/guzhou_diaoke/article/details/8201542。本文的人机对弈黑白棋游戏用Python tkinter的Canvas实现图形界面,计算机落子算法程序基本沿用前边两文。感觉采用Canvas实现图形界面棋类游戏,有如下优点:
1)Canvas图形界面能做到和pyGame图形界面同样漂亮。
2)游戏运行不要求Python安装pyGame库。
3)不像pyGame要连续刷屏,采用事件驱动方式工作,极大减少占用CPU时间。
4)利用Canvas中所增加的图形和图像对象属性tag,方便删除图形和图像对象,能更容易地为玩家提供提示标志,例如用鼠标移动象棋的棋子,点棋子选中该棋子,要加一个标志,再点一个空位,也要增加一个标志,移动后要消除这些标志。希望本游戏程序所采用的方法,能给那些希望用Python tkinter的Canvas编写棋类游戏的网友提供一些帮助。有不足之处,也请指正,万分感谢。
首先介绍黑白棋游戏规则。棋盘是由8行8列方格组成。黑白棋有黑白两色棋子。每次落子,要把本方颜色棋子放在棋盘未放棋子方格中,若在横、竖、斜八个方向的任一方向方格中有本方棋子,则被夹在中间的对手棋子全部翻转为本方棋子颜色。仅在可以翻转对方棋子的未放棋子方格才能落子。如果一方有合法落子方格,就必须落子,不得弃权。棋盘已满或双方都没有合法落子方格时棋局结束,棋子多的一方获胜。在棋盘还没有下满时,如果一方的棋子已经被对方吃光,则棋局也结束,将对手棋子吃光的一方获胜。两位玩家轮流下棋,如一方没有符合规则的落子方格,在这种情况下,另一方继续下棋,直到另一方对手有了可以落子的方格。以后恢复两者轮流下棋的顺序。
游戏结束的条件及判据:
1)整个棋盘满了。将导致双方都无法继续放棋子。
2)将对方棋子吃光,使对方得分为0。将导致双方都无法按规则继续放棋子。
3)双方轮流放棋子导致某种棋子布局,使双方都不能按规则继续放棋子。
这三种情况都以棋盘上棋子多的一方获胜。判断游戏是否结束,可用“是否双方都无法按规则继续放棋子”作为判据判断游戏是否结束。本程序采用此判据。
这里介绍计算机选择落子方格的策略,即AI算法。在计算机所有可按规则落子的方格中,认为棋盘四个角是最佳落子方格,因其不会被翻转,如可选方格中有四个角,要首先选择在四个角落子。其次对每个可能的落子方格,都要计算在该方格落子的得分 (可以翻转对手棋子的数量),分值越高则在该方格落子越有利。应选择得分最多的方格落子。
本黑白棋游戏界面如下。图1是开始界面,其中多选按钮“加标记否”已被选中。玩家先手,放黑棋子,接着计算机放白棋子,在这之后,每当玩家放黑棋子前,将提前为下一步所有可能放黑棋子的空方格上做一标记。单击多选按钮,将使其不被选中,以后就将不给出这个提示。图2是玩家和计算机各放1个棋子后的界面。带+棋子是计算机放的白子,带*棋子是计算机翻转的黑子,使玩家清楚看到前边计算机是如何放白子和翻转黑棋子的。带~的空方格是提示玩家下一步所有可能放棋子的空方格。图3显示双方平局的界面,实际上计算机赢的概率较高。
在这里插入图片描述

本游戏用全局2维列表mainBoard[x][y]记录在8*8棋盘上第x行第y列方格处放的是黑子、白子还是空,如棋盘相应方格是黑子,列表值记录为’black’,白子记录为’white’,无子记录为’none’。putPiece(board,x,y,tile,p)函数完成实际的放子工作,参数分别是:记录棋子分布的列表、棋子放到x行y列、tile指定放黑子还是白子,p是棋子图形。在函数中的2维列表dupeBoard是临时的,计算机用来在计算某方格中放子所得分数,退出函数就不存在了。如函数有列表参数,参数名称为board。allPieces是字典,键是棋子所在方格的行列数,值是该棋子对象的ID,删除某一棋子或增加一棋子要用到这个字典。在程序中定义了一个Canvas对象,其ID为w,棋盘和棋子都放Canvas对象上。在棋盘放子本质上是调用w.create_image(x1,y1,image,tag)建立image指定的图像对象放到Canvas对象上,而通过为属性tag赋值,将极大简化删除图像对象和增加提示标志的代码。例如在删除所有属性tag为p的棋子用语句:w.delete(“p”)。还还应指出的是,这里的(x1,y1)是Canvas对象坐标系,坐标单位是屏幕点距,原点(0,0)在Canvas对象左上角,x轴正方向指向屏幕右侧,y轴正方向指向屏幕下侧,x1和y1最大值是Canvas对象宽和高减1。程序中用公式将棋子的行列数转变为Canvas对象坐标值。
先看程序的主框架。程序有全局变量turn,决定某时刻是轮到玩家放棋子,还是轮到计算机放棋子。它有3个值,'player’玩家放子,为初始值、'computer’计算机放子以及"n"计算机正在放子,不允许再发自定义事件。游戏运行后,玩家用鼠标点击棋盘先放黑棋子,启动鼠标单击事件函数mouseClick,完成后令turn=‘computer’。在另一线程中重复运行的函数count,不断查询turn==‘computer’,查到时发自定义事件,启动自定义事件函数computerPlay,完成计算机放子,完成后令turn=‘player’。如此重复。请参看博文“用Python tkinter的Canvas设计的人机对弈棋类游戏中使双方棋子前后出现”。
下边是黑白棋游戏所有代码,代码有注释,希望能看明白。AI算法代码解释也可参考“Python游戏编程快速上手”一书有关章节。仅拷贝这些代码还不能使程序正常运行,必须在黑白棋游戏程序所在文件夹中建立文件夹:pic。将黑白棋棋盘.png、围棋白棋子.png和围棋黑棋子.png三个文件拷贝到pic文件夹中。注意,棋子图像文件必须是背景透明的。棋盘大小为:372373,棋子大小为:4040。可自制三个文件。在程序后边,上传了这3个文件,不知可用否。也上传了完整程序,下载后直接可用,网址是:

from threading import Timer
import time
import tkinter as tk
import random
import copy                              #使用deepcopy函数拷贝2维数组必须引用copy
#将tile指定的黑或白棋子放到棋盘x行y列方格处,p为相应图像,board是记录棋盘中棋子如何分布的2维列表
def putPiece(board,x,y,tile,p):           #tile='black'代表黑棋子,tile='white'为白棋子
    board[x][y] = tile       #8*8棋盘x行y列方格处放置由tile指定的黑或白棋子信息保存到2维列表board中
    #将棋子图像p放到由棋盘坐标(x,y)转换为Canvas点阵坐标处。44和45是棋盘边界距Canvas边界的x和Y方向距离
    a=w.create_image((44+x*40),(45+y*40),image=p,tag="p")           #棋盘单元格宽和高都是40  
    allPieces[x,y]=a        #将该图像对象ID保存的字典allPieces
# 开局时建立新棋盘,'none'表示无子,'black'代表黑棋子,'white'为白棋子
def getNewBoard():
    board = []
    for i in range(8):
        board.append(['none'] * 8)
    putPiece(board,3,3,'black',pb)      #在棋盘3行3列方格处放黑棋子
    putPiece(board,3,4,'white',pw)
    putPiece(board,4,3,'white',pw)
    putPiece(board,4,4,'black',pb)
    return board
# 在棋盘xstart行ystart列方格处放由tile指定黑或白棋子是否符合规则,合规返回需翻转对方棋子,否则返回False
def isValidMove(board, tile, xstart, ystart):       #board是记录棋盘中棋子如何分布的2维列表
    # 如果该位置已经有棋子或者出界了,返回False
    if not isOnBoard(xstart, ystart) or board[xstart][ystart] != 'none':
        return False 
    # 临时将tile 放到指定的位置
    board[xstart][ystart] = tile 
    if tile == 'black':                 #设定本方棋子和对方棋子
        otherTile = 'white'
    else:
        otherTile = 'black'
    # 得到8个方向要被翻转的对方棋子的行列数保存到列表tilesToFlip
    tilesToFlip = []        #下句,如(xdirection,ydirection)=[0, 1],从落子处向右侧查找,即每次列值+1
    for xdirection, ydirection in [ [0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1] ]:
        x, y = xstart, ystart                           #落子处的行列数
        x += xdirection
        y += ydirection
        if isOnBoard(x, y) and board[x][y] == otherTile:  #行列合法值0-7,如果行列数都不出界并且该方格有对方棋子
            x += xdirection                               
            y += ydirection
            if not isOnBoard(x, y):                       #下一位置如出界,此方向没有要翻转的对方棋子,查下一方向
                continue
            # 一直走到出界或不是对方棋子的位置
            while board[x][y] == otherTile:
                x += xdirection
                y += ydirection
                if not isOnBoard(x, y):
                    break
            # 出界了,则没有棋子要翻转OXXXXX       O是己方,X是对方
            if not isOnBoard(x, y):
                continue
            # 是自己的棋子OXXXXXXO              中间的X都要翻转,从最后X开始,逐一向前所有X的行列数都要保存到列表
            if board[x][y] == tile:             #如是己方棋子,说明此方向排列为:OXXXXXXO
                while True:
                    x -= xdirection
                    y -= ydirection
                    # 回到了起点则结束
                    if x == xstart and y == ystart:
                        break
                    # 需要翻转的棋子
                    tilesToFlip.append([x, y])
    # 将前面临时放上的棋子去掉,即还原棋盘
    board[xstart][ystart] = 'none' # restore the empty space
    # 没有要被翻转的棋子,则走法非法。翻转棋的规则。
    if len(tilesToFlip) == 0:   # If no tiles were flipped, this is not a valid move.
        return False
    return tilesToFlip
# 是否出界
def isOnBoard(x, y):
    return x >= 0 and x <= 7 and y >= 0 and y <=7
# 获取可落子的位置
def getValidMoves(board, tile):
    validMoves = []
    for x in range(8):
        for y in range(8):
            if isValidMove(board, tile, x, y) != False:
                validMoves.append([x, y])
    return validMoves
# 获取棋盘上黑白双方的棋子数
def getScoreOfBoard(board):
    xscore = 0
    oscore = 0
    for x in range(8):
        for y in range(8):
            if board[x][y] == 'black':
                xscore += 1
            if board[x][y] == 'white':
                oscore += 1
    return {'black':xscore, 'white':oscore}     #返回字典
# 将一个tile棋子放到(xstart, ystart)
def makeMove(board, tile, xstart, ystart,p):
    tilesToFlip = isValidMove(board, tile, xstart, ystart)          #得到要翻转的所有对方棋子位置
    if tilesToFlip == False:         #如果列表为空,即无可翻转的对方棋子,放子非法。注意bool([])==False
        return False
    putPiece(board,xstart,ystart,tile,p)            #放棋子
    for x, y in tilesToFlip:                        #翻转对方棋子
        w.delete(allPieces[x,y])                    #删除对方棋子
        putPiece(board,x,y,tile,p)                  #放己方棋子
    if tile==computerTile: #为计算机白棋增加放子标记,+为计算机放的白子,*是被反转的黑子,注意tag都是"A"
        w.create_text((44+xstart*40),(45+ystart*40), text = "+",fill="red",tag="A",font=("Arial",20))
        for x, y in tilesToFlip:
            w.create_text((44+x*40),(45+y*40), text = "*",fill="red",tag="A",font=("Arial",20))
    return True
# 是否在角上
def isOnCorner(x, y):
    return (x == 0 and y == 0) or (x == 7 and y == 0) or (x == 0 and y == 7) or (x == 7 and y == 7)
# 电脑走法,AI,返回计算机放棋子的最优解
def getComputerMove(board, computerTile):
    # 获取计算机所有可放棋子的位置
    possibleMoves = getValidMoves(board, computerTile)
    # 打乱所有可放棋子的位置的顺序,使计算机放棋子方法无规律可循
    random.shuffle(possibleMoves)
    # [x, y]在角上,则优先走,因为角上的不会被再次翻转
    for x, y in possibleMoves:
        if isOnCorner(x, y):
            return [x, y]
    #以下在临时棋盘每一可放子位置放子,并计算总得分数,找到得分最高放子位置
    bestScore = -1 
    for x, y in possibleMoves:
        dupeBoard = copy.deepcopy(board)     #得到临时棋盘,使用deepcopy函数拷贝2维数组必须引用copy
        tilesToFlip = isValidMove(dupeBoard,computerTile, x, y)#得到所有翻转对方棋子位置
        dupeBoard[x][y] = computerTile       #在临时棋盘放棋子
        for x1, y1 in tilesToFlip:           #在临时棋盘翻转对方棋子
            dupeBoard[x1][y1] = computerTile
        score = getScoreOfBoard(dupeBoard)[computerTile]            #计算放置并翻转棋子后的得分
        if score > bestScore:                #按照分数选择走法,优先选择翻转后总分数最多的走法
            bestMove = [x, y]
            bestScore = score
    return bestMove
 
def showGameEnd(board):
    gameOver=True
    score=getScoreOfBoard(board)                        #得到双方分数,注意返回字典
    if score[computerTile]>score[playerTile]:           #score[computerTile]为计算机得分
        label['text']="游戏结束\n玩家输了"
    elif score[computerTile]<score[playerTile]:         #score[playerTile]为玩家得分
        label['text']="游戏结束\n玩家赢了"
    else:
        label['text']="游戏结束\n平局"

def showScoe(board):                      #显示分数
    score=getScoreOfBoard(board)          #得到双方分数,注意返回字典
    label1['text']="计算机得分:\n"+str(score[computerTile])  #score[computerTile]为计算机得分
    label2['text']="玩家得分:\n"+str(score[playerTile])      #score[playerTile]为玩家得分         
#玩家用鼠标点击棋盘,启动鼠标点击事件函数,放黑子
def mouseClick(event):                  #event.x,event.y鼠标左键的x,y坐标
    global gameOver,turn
    if turn == 'player' and gameOver==False:
        label['text']=""                #删除前边提示信息
        x,y=(event.x),(event.y) #x,y可能非法包括:该位置有子,无子但不能反转白子,不在方格内
        col = int((x-23)/40)    #23为8*8网格图右边在x轴方向距背景图左边距离
        row = int((y-24)/40)    #24为8*8网格图上边在y轴方向距背景图上边距离,40为方格长和宽
        if makeMove(mainBoard,playerTile,col,row,pb)==False:#放黑子,如位置非法,不能放子
            return                                          #玩家重新放子,提示不变
        showScoe(mainBoard)                                 #到此处玩家已放子,显示分数
        w.delete("A")                                       #到此处玩家已放子,可去掉提示
        if getValidMoves(mainBoard, computerTile) != []:    #返回计算机可放子位置,如有子可放
            turn = 'computer'                               #转计算机放子,游戏一定未结束
            return
        #到此电脑无子可放,如玩家也无子可放,可能已放64子或某方得分为0,或棋子布局使双方都不能放子
        #因此游戏结束,根据得分判断输赢。否则提示:电脑无子可放,玩家放子,然后退出本函数
        if getValidMoves(mainBoard, playerTile) == []:      #返回玩家可放子位置,如无子可放
            showGameEnd(mainBoard)                          #游戏结束
        else:
            label['text']="电脑无子可放\n玩家放子" #到此计算机无子可放,玩家有子可放,提示玩家放子 
            makePlayerMark()        #返回下次玩家可放棋子所有位置

def makePlayerMark():               #返回下次玩家可放棋子所有位置,并为这些位置做标记
    possibleMoves = getValidMoves(mainBoard, playerTile)    #得到玩家可放子所有位置    
    if  possibleMoves!= [] and v.get()==1:                  #如玩家有子可放及复选框"加标记否"选中
        for x, y in possibleMoves:                          #在玩家可放子所有位置做标记
            w.create_text((44+x*40),(45+y*40), text = "~",tag="A",font=("Arial",20))
    return  possibleMoves                                   #返回下次玩家可放棋子所有位置                                              
#收到自定义事件函数,调用本函数,计算机放白子
def computerPlay(event):
    global gameOver,turn   
    if turn == 'computer' and gameOver==False:
        turn='n'#在运行函数computerPlay时,不允许再发事件makeComputerPlay,再次调用computerPlay                               
        x, y = getComputerMove(mainBoard, computerTile)         #计算放白子最优位置在(x,y)
        makeMove(mainBoard, computerTile,x,y,pw)                #计算机放子
        showScoe(mainBoard)                                      #到此处计算机已放子,显示分数
        if makePlayerMark()!= []:#makePlayerMark返回下次玩家可放棋子所有位置,并为这些位置做标记
            turn = 'player'                        #如下次玩家有放棋子的位置,turn = 'player'
            return                                                  
        if getValidMoves(mainBoard, computerTile) == []:       #到此玩家无子可放,如计算机也无子可放
            showGameEnd(mainBoard)                             #游戏结束
        else:                                      #如此玩家无子可放,计算机有子可放,计算机继续放棋子
            label['text']="玩家无子可放\n电脑放子"
            turn = 'computer'         #令turn=='computer'时,允许再发事件makeComputerPlay  

def count():                #该函数完成每秒查看是否轮到计算机放白子功能,将运行在子线程中
    global turn
    while True:
        if turn == 'computer':                              #如轮到计算机放白子
            root.event_generate('<<makeComputerPlay>>')     #发消息启动计算机放白子程序
        time.sleep(1)
def playAgain():
    global w,allPieces,turn,gameOver,playerTile,computerTile,mainBoard,timer
    w.delete("p")#删除所有棋子,见博文"在Python tkinter的Canvas画布上删除所有相同tag属性对象的方法"
    w.delete("A")               #删除所有提醒标志
    allPieces={}                #清空所有棋子的引用ID
    turn = 'player'             #玩家先放棋子
    gameOver = False
    playerTile = 'black'        #玩家使用黑子
    computerTile = 'white'      #计算机使用白子
    label['text']='玩家先放子'
    label1['text']='计算机得分:\n0'
    label2['text']='玩家得分:\n0'
    mainBoard = getNewBoard()   #建立8*8新棋盘,初始值为"none",放2白2黑共棋子,返回ID
    timer=Timer(1, count)       #执行timer.start()语句后,1秒后调用count函数在另一线程运行
    timer.setDaemon(True)       #使主进程结束后子线程也会随之结束。
    timer.start()

root = tk.Tk()                      #初始化窗口
root.title('黑白棋')                 #窗口标题
root.geometry("455x371+200+20") #窗口宽450,高=373,窗口左上点离屏幕左边界200,离屏幕上边界距离20。
root.resizable(width=False,height=False)    #设置窗口是否可变,宽不可变,高不可变,默认为True
w = tk.Canvas(root, width = 372, height = 373, background = "white")        #创建Canvas对象
w.pack(side=tk.LEFT, anchor=tk.NW)                          #放置Canvas对象在root窗体左上角
w.bind("<Button-1>",mouseClick)                             #画布与鼠标左键单击事件绑定
root.bind("<<makeComputerPlay>>",computerPlay) 	  #将自定义事件makeComputerPlay和事件函数绑定
#拷贝所有代码不能运行,必须找到这3个文件,存到运行程序所在文件夹下子文件夹pic中!!!!,也可下载本人程序
pp = tk.PhotoImage(file='pic/黑白棋棋盘.png')       #围棋棋盘图像宽和高为372x373像素
pw = tk.PhotoImage(file='pic/围棋白棋子.png')       #棋子图像必须是png格式,其背景必须是透明的
pb = tk.PhotoImage(file='pic/围棋黑棋子.png')   
w.create_image(0,0, image=pp,anchor = tk.NW)       #放置棋盘图像对象在Canvas对象左上角
label=tk.Label(root,font=("Arial",10))
label.place(x=375,y=2,width=90,height=30)
label1=tk.Label(root,font=("Arial",10),text='计算机得分:0')
label1.place(x=375,y=52,width=80,height=30)
label2=tk.Label(root,font=("Arial",10),text='玩家得分:0')
label2.place(x=375,y=102,width=70,height=30)
button=tk.Button(root,text="重玩游戏", command=playAgain)
button.place(x=379,y=250,width = 70,height = 20)
v = tk.IntVar()
c1=tk.Checkbutton(root,variable = v,text = "加标记否")
c1.place(x=379,y=300,width = 70,height = 30)
v.set(1)        #Checkbutton被设置为选中,将为玩家提示可放棋子位置
playAgain()         #游戏初始化
root.mainloop()	

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242

下边是上传的棋盘、黑子和白子的图形,右击图形,选择另存为,保存到建立的pic文件加中,并改名为:黑白棋棋盘、围棋白棋子和围棋黑棋子,扩展名为png。希望下载的文件可用。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/135280
推荐阅读
相关标签
  

闽ICP备14008679号