赞
踩
在“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()
下边是上传的棋盘、黑子和白子的图形,右击图形,选择另存为,保存到建立的pic文件加中,并改名为:黑白棋棋盘、围棋白棋子和围棋黑棋子,扩展名为png。希望下载的文件可用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。