目录
一、效果预览
- 主页面
- 游戏页面
二、预处理
- 导入模块
import itertools
from tkinter.messagebox import *
from tkinter import *
import random
- 函数声明
在本程序中使用了几个自定义的函数,这些函数的功能及声明形式代码如下:
自定义函数 | 功能 |
---|---|
def start(self, event) | 初始化游戏页面 |
def callback1(self, event) | 双人模式控制黑棋 |
def callback2(self, event) | 双人模式控制白棋 |
def rand(self, event) | 单人模式控制黑棋 |
def check_win(self, x, y) | 判断胜负 |
def is_repeat(self, x, y) | 判断棋子是否重复 |
def destroy_buttons(self) | 销毁按钮,防止误触从而重新进入游戏界面 |
def is_continue_game(self, event) | 游戏结束后,判断用户是否继续游戏还是结束游戏 |
def regret_chess(self, event) | 悔棋 |
三、init()函数
在程序中,可以把一局游戏看作一个
class
,每个环节是它的一个函数,而__init__()
的函数总是在class被初始化时执行
它的主要功能是:传递参数,比如赋值给对象属性
功能:
- 为游戏创建了一个基于Tkinter的GUI窗口
- 添加了三个按钮用于选择游戏模式和退出游戏
- 由于进入游戏后防止误触以及不美观等因素,需要创建一个列表来保存按钮的引用,以便销毁双人模式和单人模式
实现代码及注释如下:
def __init__(self):
self.root = Tk() # 创建窗口
self.root.geometry("1000x1000") # 窗口大小
self.r = Canvas(self.root, width=1000, height=1000) # 创建画布
self.r.pack(pady=20) # 画布位置
self.pic = PhotoImage(file='D:\\code\\2024\\Python\\python\\task\\Gomoku_game\\data\\Gomoku_main_page.png')
self.r.create_image(500, 450, image=self.pic) # 图片位置
# 创建一个退出游戏的按钮,以便退出游戏
self.b1 = Button(self.root, text="退出游戏", width=8, font=("楷体", 13), command=self.root.destroy)
self.b1.pack() # 显示
self.b1.place(x=870, y=950) # 位置
self.buttons = [] # 创建一个列表来保存按钮的引用,以便销毁
s = ["双人", "单人"]
for i in range(len(s)):
self.b = Button(self.root, text=s[i], width=10, font=("楷体", 17)) # 添加按钮
self.b.bind('<Button-1>', self.start) # 绑定self.start方法
self.b.place(x=(i + 1) * 300, y=950) # 按钮位置
self.buttons.append(self.b) # 将按钮引用添加到列表中
self.root.mainloop() # 运行窗口
四、自定义函数
1. def start()
功能:
- 销毁以前的画布,并新建一个画布绘制棋盘和交叉点,初始化棋盘
- 创建悔棋按钮,并绑定到regret_chess方法
- 根据被点击的模式选择按钮,将画布上的鼠标点击事件绑定到不同的方法。
- 随后销毁选择模式的按钮
def start(self, event):
self.r.destroy() # 销毁上面的画布,为了进入游戏
self.c = Canvas(self.root, width=1000, height=930) # 创建新的画布
self.c.pack()
self.c.place(x=0, y=0) # 防止用户重复点击造成画布位置移动
self.pic = PhotoImage(file='D:\\code\\2024\\Python\\python\\task\\Gomoku_game\\data\\Gomoku_background.png')
self.c.create_image(400, 340, image=self.pic) # 图片位置
self.r_chess = Button(self.root, text="悔棋", width=8, font=("楷体", 17)) # 放置悔棋按钮
self.r_chess.bind('<Button-1>', self.regret_chess) #绑定一个方法
self.r_chess.pack()
self.r_chess.place(x=447, y=940)
# 绘制棋盘网格
for i in range(1, 16): # 绘制棋盘的水平和垂直线
self.c.create_line(60, 60 * i, 900, 60 * i)
self.c.create_line(60 * i, 60, 60 * i, 900)
# 绘制棋盘的交叉点
for i in range(60, 901, 60):
for j in range(60, 901, 60):
self.c.create_oval(i - 2, j - 2, i + 2, j + 2, fill="black") # 在棋盘的每个交叉点上绘制一个小的蓝色圆形
self.matrix = [[0 for y in range(16)] for x in range(16)] # 初始化矩阵的每个位置为0,用于存储棋盘的状态
# 实际上棋子是落在前14x14,为防止白子的自动生成造成越界访问将其初始化为16x16
# isinstance()方法:检查了widget是否真的是一个Button实例
if isinstance(event.widget, Button) and (event.widget["text"]) == "双人":
self.c.bind("<Button-1>", self.callback1) # 如果点击双人使用左键,调用callback1
self.c.bind("<Button-3>", self.callback2) # 如果点击双人使用右键,调用callback2
elif isinstance(event.widget, Button) and (event.widget["text"]) == "单人":
self.c.bind("<Button-1>", self.rand) # 如果左键点击单人,调用rand
self.destroy_buttons() # 点击之后进入游戏,因此需要销毁按钮
2. def callback1() / def callback2()
功能:
- 使用
try...except
块来捕捉可能出现的异常,确保程序不会因为意外情况而崩溃- 全局变量
temp
:使用 global temp 声明全局变量,用于跟踪当前是黑方还是白方的回合(callback1为黑方,callback2为白方)- 判断当前位置是否有棋子,若无,则在棋盘上放置一个黑子,更新矩阵并在画布上绘制一个黑子
- 判断是否有玩家获胜
- 如果用户点击了棋盘以外的地方,提示用户
def callback1(self, event):
try: # 捕捉异常防止崩溃
global temp #声明全局变量
if temp % 2 == 0: #为偶数
u, v = event.x, event.y # 获取鼠标点击的位置:从event中提取鼠标的x和y坐标,并分别赋值给u和v
temp += 1
else:
showinfo("ERROR", "现在是白方回合!")
# 确定鼠标点击的棋盘格子
for i in range(1, 16): # 用于确定x坐标(即u)对应的棋盘列索引zx
if 30 * i < u < 30 * (2 * i + 1):
zx = i - 1
break
for i in range(1, 16): # 用于确定y坐标(即v)对应的棋盘行索引zy
if 30 * i < v < 30 * (2 * i + 1):
zy = i - 1
break
self.zx = zx # 存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = 1 # 在棋盘上放黑子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20,
(zx + 1) * 60 + 20, (zy + 1) * 60 + 20, fill="black") # 绘制棋子
win = self.check_win(zx, zy) # 判断是否有玩家获胜,传入棋子的位置
if win == 1:
showinfo("Game over", "黑方获胜!")
temp += 1 #如果黑方获胜,那么黑方必定是最后一个落子,要控制黑方先手temp+=1
self.start(event) # 重置游戏状态
self.is_continue_game(event)
except UnboundLocalError as u:
print(u)
except Exception as e:
showwarning("Warning", "请在棋盘上落子!")
def callback2(self, event):
try: # 捕捉异常防止误触
global temp # 使得temp可更改
if temp % 2 != 0: # 为奇数
u, v = event.x, event.y # 获取鼠标点击的位置:从event中提取鼠标的x和y坐标,并分别赋值给u和v
temp += 1
else:
showinfo("ERROR", "现在是黑方回合!")
# 确定鼠标点击的棋盘格子
for i in range(1, 16):
if 30 * i < u < 30 * (2 * i + 1):
zx = i - 1
break
for i in range(1, 16):
if 30 * i < v < 30 * (2 * i + 1):
zy = i - 1
break
self.zx = zx # 存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = -1 # 在棋盘上放白子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20,
(zx + 1) * 60 + 20, (zy + 1) * 60 + 20, fill="white") # 绘制椭圆并保存其ID
win = self.check_win(zx, zy) # 判断胜负
if win == -1:
showinfo("Game over", "白方获胜!")
self.start(event) # 重置游戏状态
self.is_continue_game(event)
except UnboundLocalError as u:
print(u)
except Exception as e:
showwarning("Warning", "请在棋盘上落子!")
print(e)
3. def rand()
功能:
- 自动落白子
- 防止自动生成的白子将原来的黑子覆盖
- 判断胜负
- 如果用户点击了棋盘以外的地方,提示用户
def rand(self, event):
u, v = event.x, event.y # 获取鼠标点击的位置
# 确定鼠标点击的棋盘格子
for i in range(1, 16):
if 30 * i < u < 30 * (2 * i + 1):
zx = i - 1
break
for i in range(1, 16):
if 30 * i < v < 30 * (2 * i + 1):
zy = i - 1
break
try: #防止用户误触 捕捉异常
self.zx = zx #存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = 1 # 在棋盘上放黑子
self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20, (zx + 1) * 60 + 20, (zy + 1) * 60 + 20,
fill="black") # 绘制棋子
# 自动落白子:为了在给定的黑子周围找到一个空位置并自动放置一个白子
# 1.遍历黑子周围的8个格子将每一个空格子的坐标添加到s中,然后从s中随机选择一个坐标,并在图形界面上的对应位置画一个白色的圆形表示白子。
# 2.为防止从s中选到了白子的位置,从而覆盖了白子,可以设置当上面没有落子的时候再绘画棋子,否则继续生成随机坐标
# 3.防止当黑子落在棋盘边缘的时候白子落在棋盘外面,如果i和j都大于0小于15(取15会落在外面),正常取值,否则如果小于0取反,大于15-2
# 4.黑子如果下在一个周围都有棋子的地方,会导致s是空列表,我们可以直接判断s是否为空,如果为空则找空随机落子,否则从s中选择落子
s = []
for i in range(zx - 1, zx + 2):
for j in range(zy - 1, zy + 2):
if self.matrix[i][j] == 0: # 如果没有落子
if 0 <= i <= 14 and 0 <= j <= 14:
L = (i, j)
else:
if i == -1:
i += 1
if j == -1:
j += 1
if i == 15:
i -= 2
if j == 15:
j -= 2
L = (i, j)
if L not in s:
s.append(L)
# 确保s不是空列表
if not s:
a, b = random.randint(0, 14), random.randint(0, 14)
while self.matrix[a][b] != 0: # 确保位置是空的
a, b = random.randint(0, 14), random.randint(0, 14)
wz = (a, b) # 生成随机坐标
else:
wz = random.choice(s) # 从s中生成随机坐标
# 为了防止自动生成的白子将原来的黑子覆盖,通过while循环判断随机生成的坐标上有无棋子,无则落子,有则重新生成直至落子成功
# 黑子如果下在一个周围都有棋子的地方,白旗没有地方放置一直生成位置,就会陷入死循环,因此我们可以使用一个count查看循环的次数
# 如果循环超过60次就在地图上随机找一个空位置放置棋子(并不是固定60,而是要给够while找到位置的时间)
count = 0
while True:
count += 1
if self.matrix[wz[0]][wz[1]] == 0:
self.oval_id = self.c.create_oval((wz[0] + 1) * 60 - 20, (wz[1] + 1) * 60 - 20,
(wz[0] + 1) * 60 + 20, (wz[1] + 1) * 60 + 20, fill="white")
self.matrix[wz[0]][wz[1]] = -1 # 将+设置随机坐标设置为白子
break
else:
wz = random.choice(s)
if count >= 60:
a, b = random.randint(0, 14), random.randint(0, 14)
if self.matrix[a][b] == 0:
self.c.create_oval((a + 1) * 60 - 20, (b + 1) * 60 - 20,
(a + 1) * 60 + 20, (b + 1) * 60 + 20, fill="white")
self.matrix[a][b] = -1 # 将+设置随机坐标设置为白子
break
# 因为这里的wz[0], wz[1]和zx,zy是两个不同的参数,因此单独判断
win_white = self.check_win(wz[0], wz[1])
win_black = self.check_win(zx, zy) # 判断胜负
if win_white == -1:
showinfo("Game over", "白方获胜!")
self.start(event)
self.is_continue_game(event)
if win_black == 1:
showinfo("Game over", "黑方获胜!")
self.start(event) # 重置游戏状态
self.is_continue_game(event)
except Exception as e:
showwarning("Warning", "请在棋盘上落子!")
print(e)
4. def check_win()
功能:
- 初始化一个列表用于判断五子连珠
- 获取四个方向的列表,并嵌入原始的列表中
- 使用
itertools.groupby()
方法检查连续棋子
def check_win(self, x, y): # 传入棋盘上的坐标x y
four_direction = [] # 初始化方向列表:存储四个方向(水平、垂直、左斜、右斜)的棋子序列
# 获取水平和垂直方向的列表:分别获取了(x, y)位置所在的行和列的棋子序列
four_direction.append([self.matrix[i][y] for i in range(15)]) # 水平
four_direction.append([self.matrix[x][j] for j in range(15)]) # 垂直
# 获取左斜方向的列表:即把左斜方向每个棋格的状态放进列表
zuox = []
for i in range(15): # 遍历棋盘找到左斜方向的棋子:根据规律可以得知,坐标之和相等放在同一组列表
for j in range(15): # self.matrix[i][j]获取格子的状态,0没有放,1黑子,-1白子
if i + j == x + y:
zuox.append(self.matrix[i][j]) # 每次下棋时,坐标之和相等放在同一组列表,若和不相等会重新创造列表
four_direction.append(zuox)
print("左斜为", zuox)
# 获取右斜方向的列表
youx = [] # 根据规律可以把坐标之差相等的棋子分为一组
if x < y: # 如果x < y,则沿着主对角线向右上方遍历,否则,沿着次对角线向右下方遍历。
for i in range(15 - (y - x)):
youx.append(self.matrix[i][i + (y - x)])
else:
for i in range(15 - (x - y)):
youx.append(self.matrix[i + (x - y)][i])
four_direction.append(youx)
print("右斜为", youx)
# 检查连续棋子
black, white = [0], [0]
for i in four_direction: # 检查每个方向上的连续棋子序列,统计每个方向上连续黑棋和白棋的数量
for key, group in itertools.groupby(i): # groupby()方法:把可迭代对象中相邻的重复元素挑出来放一起
if key == 1: # 在这里是将每个方向上相邻的重复棋子放在一起,查看哪个棋子的数量先达到5个
black.append(len(list(group)))
elif key == -1:
white.append(len(list(group)))
if max(black) >= 5:
return 1
elif max(white) >= 5:
return -1
5. def is_repeat()
功能:
- 判断重复落子,如果是空位置返回
True
,反之返回False
- 使用temp控制先后手,防止误触导致的先后手顺序错乱
# 当落子的时候,他被赋值为1或-1,可以通过判断他是否有棋子来判断能否点击,如果不能点击,阻止赋值操作和绘画棋子并提示用户
# 如果坐标上面没有棋子,返回True可以进行赋值和绘画,反之返回False
def is_repeat(self, x, y):
global temp
if self.matrix[x][y] == 0:
return True
else:
temp -= 1 # 控制先后手
showwarning("ERROR!", "落子错误,请重新落子!")
return False
pass
6. def destroy_buttons()
功能:
- 销毁按钮,防止误触从而重新进入游戏界面
- 清空储存的按钮列表,以防之后重复销毁导致崩溃
# 销毁按钮,防止误触从而重新进入游戏界面
def destroy_buttons(self):
for button in self.buttons:
button.destroy() # 销毁每个按钮
self.buttons.clear() # 清空按钮列表,以防之后重复销毁
7. def is_continue_game()
功能:
- 游戏结束后,创建一个提示框用于判断用户是否继续游戏还是结束游戏
- 如果继续(Y),继续创建一个提示框用于选择模式
- 如果退出(N),直接destroy
# 游戏结束后,判断用户是否继续游戏还是结束游戏
def is_continue_game(self, event):
answer = askquestion("Game over", "是否继续游戏?")
if answer == "yes":
self.start(event)
# 因为前面销毁了游戏模式按钮,因此需要重新选择游戏模式
answer1 = askyesno("选择模式", "Y(双人)/N(单人)")
if answer1:
self.start(event)
self.c.bind("<Button-1>", self.callback1) # 上面有讲解
self.c.bind("<Button-3>", self.callback2)
else:
self.start(event)
self.c.bind("<Button-1>", self.rand)
else:
self.root.destroy()
8. def regret_chess()
功能:
- 在棋盘旁边添加一个悔棋按钮
- 判断按钮是否被点击,如果点击了删除椭圆
# 悔棋:在棋盘旁边添加一个悔棋按钮,点击一下,删除椭圆
# 放置棋子并保存坐标and绘制椭圆并保存其ID
# 使用if判断是否按钮被点击,如果点击了使用图形ID删除椭圆,并且通过坐标把原来的赋值更改为0
def regret_chess(self, event):
global temp
answer = askyesno("悔棋", "是否要悔棋?")
if answer:
temp -= 1 # 操控先后手
self.matrix[self.zx][self.zy] = 0
self.c.delete(self.oval_id)
else:
pass
五、主要功能的具体实现
1.先后手判定
- 思路:
- 双人模式下,黑白棋一棋一回合
- 黑棋先手,如果轮到他人的回合,则他们的鼠标位置就不能被获取,并且输出提示信息
- 可通过一个数
temp
统计步数,从0开始,如果temp为奇数,则是黑子的回合,若是偶数则是白子回合,如果悔棋则temp - 1
- 每次落子都
temp += 1
,temp=1为黑子 - 最后,把temp放在需要使用的函数中,并声明
global temp
例如:
global temp #使得temp可更改
if temp % 2 == 0: #为偶数
u, v = event.x, event.y # 获取鼠标点击的位置:从event中提取鼠标的x和y坐标,并分别赋值给u和v
temp += 1
else:
showinfo("ERROR", "现在是白方回合!")
2.悔棋
- 思路:
- 在棋盘旁边添加一个悔棋按钮,点击一下,调用
regret_chess()
方法删除椭圆 - 定义一个
regret_chess()
方法传入落子的位置- 传入落子位置:通过在回调函数中放置
self.zx = zx
和self.zy = zy
存储棋子坐标 - 删除棋子:在绘制棋子的时候保存其ID
self.oval_id
, - 创建一个判断框询问是否悔棋,如果是,将棋子删除并且将坐标的值置为0
- 传入落子位置:通过在回调函数中放置
- 在棋盘旁边添加一个悔棋按钮,点击一下,调用
bug:
仅在双人模式下生效且每次落子只能使用一次,在单人模式下,只会删除电脑落子
例如:
self.zx = zx # 存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = 1 # 在棋盘上放黑子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20,
(zx + 1) * 60 + 20, (zy + 1) * 60 + 20, fill="black") # 绘制棋子
3.自动落白子
- 思路:
- 为了在给定的黑子周围找到一个空位置并自动放置一个白子
- 遍历黑子周围的8个格子将每一个空格子的坐标添加到s中,然后从s中随机选择一个坐标,并在图形界面上的对应位置画一个白色的圆形表示白子
- 为防止从s中选到了白子的位置,从而覆盖了白子,可以设置当上面没有落子的时候再绘画棋子,否则继续生成随机坐标
- 防止当黑子落在棋盘边缘的时候白子落在棋盘外面,如果i和j都大于0小于15(取15会落在外面),正常取值,否则如果小于0取反,大于15-2
- 黑子如果下在一个周围都有棋子的地方,会导致s是空列表,我们可以直接判断s是否为空,如果为空则找空随机落子,否则从s中选择落子
- 为了在给定的黑子周围找到一个空位置并自动放置一个白子
s = []
for i in range(zx - 1, zx + 2):
for j in range(zy - 1, zy + 2):
if self.matrix[i][j] == 0: # 如果没有落子
if 0 <= i <= 14 and 0 <= j <= 14:
L = (i, j)
else:
if i == -1:
i += 1
if j == -1:
j += 1
if i == 15:
i -= 2
if j == 15:
j -= 2
L = (i, j)
if L not in s:
s.append(L)
# 确保s不是空列表
if not s:
a, b = random.randint(0, 14), random.randint(0, 14)
while self.matrix[a][b] != 0: # 确保位置是空的
a, b = random.randint(0, 14), random.randint(0, 14)
wz = (a, b) # 生成随机坐标
else:
wz = random.choice(s) # 从s中生成随机坐标
4.防止自动生成的白子将原来的黑子覆盖
- 思路:
- 通过while循环判断随机生成的坐标上有无棋子,无则落子,有则重新生成直至落子成功
- 黑子如果下在一个周围都有棋子的地方,白旗没有地方放置一直生成位置,就会陷入死循环,因此我们可以使用一个count查看循环的次数
- 如果循环超过60次就在地图上随机找一个空位置放置棋子(并不是固定60,而是要给够while找到空位置落子的时间)
count = 0
while True:
count += 1
if self.matrix[wz[0]][wz[1]] == 0:
self.oval_id = self.c.create_oval((wz[0] + 1) * 60 - 20, (wz[1] + 1) * 60 - 20,
(wz[0] + 1) * 60 + 20, (wz[1] + 1) * 60 + 20, fill="white")
self.matrix[wz[0]][wz[1]] = -1 # 将+设置随机坐标设置为白子
break
else:
wz = random.choice(s)
if count >= 60:
a, b = random.randint(0, 14), random.randint(0, 14)
if self.matrix[a][b] == 0:
self.oval_id = self.c.create_oval((a + 1) * 60 - 20, (b + 1) * 60 - 20,
(a + 1) * 60 + 20, (b + 1) * 60 + 20, fill="white")
self.matrix[a][b] = -1 # 将+设置随机坐标设置为白子
break
5.判断重复落子
- 思路:
- 通过
is_repeat
方法的返回值- 当落子的时候,这个位置的值被赋值为1或-1,可以通过判断他有棋子来判断能否点击,如果不能点击,阻止赋值操作和绘画棋子并提示用户
- 如果坐标上面没有棋子,返回True可以进行赋值和绘画,反之返回False
- 通过
例如:
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = 1 # 在棋盘上放黑子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20,
(zx + 1) * 60 + 20, (zy + 1) * 60 + 20, fill="black") # 绘制棋子
6.销毁按钮
- 思路:
- 创建一个列表来保存按钮的引用,并将按钮引用添加到列表中
- 随后调用
destroy_buttons()
方法销毁按钮
self.buttons = [] # 创建一个列表来保存按钮的引用,以便销毁
s = ["双人", "单人"]
for i in range(len(s)):
self.b = Button(self.root, text=s[i], width=10, font=("楷体", 17)) # 添加按钮
self.b.bind('<Button-1>', self.start) # 绑定self.start方法
self.b.place(x=(i + 1) * 300, y=950) # 按钮位置
self.buttons.append(self.b) # 将按钮引用添加到列表中
def destroy_buttons(self):
for button in self.buttons:
button.destroy() # 销毁每个按钮
self.buttons.clear() # 清空按钮列表,以防之后重复销毁导致崩溃
整体代码
import itertools
from tkinter.messagebox import *
from tkinter import *
import random
# 双人模式下,黑白棋一棋一回合
# 思路:黑棋先手,如果轮到他人的回合,则他们的鼠标位置就不能被获取,并且输出提示信息
# 可通过一个数temp控制,从0开始,如果temp为奇数,则是黑子的回合,若是偶数则是白子回合,如果悔棋则temp-1
# 每次落子都temp+1,temp=1为黑子
temp = 0 # 控制先后手
class GomokuGame:
# 为游戏创建了一个基本的Tkinter窗口,并添加了两个按钮用于选择游戏模式
def __init__(self):
self.root = Tk() # 创建窗口
self.root.geometry("1000x1000") # 窗口大小
self.r = Canvas(self.root, width=1000, height=1000) # 创建画布
self.r.pack(pady=20) # 画布位置
self.pic = PhotoImage(file='D:\\code\\2024\\Python\\python\\task\\Gomoku_game\\data\\Gomoku_main_page.png')
self.r.create_image(500, 450, image=self.pic) # 图片位置
# 创建一个退出游戏的按钮,以便退出游戏
self.b1 = Button(self.root, text="退出游戏", width=8, font=("楷体", 13), command=self.root.destroy)
self.b1.pack() # 显示
self.b1.place(x=870, y=950) # 位置
self.buttons = [] # 创建一个列表来保存按钮的引用,以便销毁
s = ["双人", "单人"]
for i in range(len(s)):
self.b = Button(self.root, text=s[i], width=10, font=("楷体", 17)) # 添加按钮
self.b.bind('<Button-1>', self.start) # 绑定self.start方法
self.b.place(x=(i + 1) * 300, y=950) # 按钮位置
self.buttons.append(self.b) # 将按钮引用添加到列表中
self.root.mainloop() # 运行窗口
# 用于初始化游戏界面
def start(self, event):
self.r.destroy() # 销毁上面的画布,为了进入游戏
self.c = Canvas(self.root, width=1000, height=930) # 创建新的画布
self.c.pack()
self.c.place(x=0, y=0) # 防止用户重复点击造成画布位置移动
self.pic = PhotoImage(file='D:\\code\\2024\\Python\\python\\task\\Gomoku_game\\data\\Gomoku_background.png')
self.c.create_image(400, 340, image=self.pic) # 图片位置
self.r_chess = Button(self.root, text="悔棋", width=8, font=("楷体", 17)) # 放置悔棋按钮
self.r_chess.bind('<Button-1>', self.regret_chess) #绑定一个事件
self.r_chess.pack()
self.r_chess.place(x=447, y=940)
# 绘制棋盘网格
for i in range(1, 16): # 绘制棋盘的水平和垂直线
self.c.create_line(60, 60 * i, 900, 60 * i)
self.c.create_line(60 * i, 60, 60 * i, 900)
# 绘制棋盘的交叉点
for i in range(60, 901, 60):
for j in range(60, 901, 60):
self.c.create_oval(i - 2, j - 2, i + 2, j + 2, fill="black") # 在棋盘的每个交叉点上绘制一个小的蓝色圆形
self.matrix = [[0 for y in range(16)] for x in range(16)] # 初始化矩阵的每个位置为0,用于存储棋盘的状态
# 实际上棋子是落在前14x14 并且为防止越界访问将其初始化为16x16 防止后面的白子生成越界
# isinstance()方法:检查了widget是否真的是一个Button实例
if isinstance(event.widget, Button) and (event.widget["text"]) == "双人":
self.c.bind("<Button-1>", self.callback1) # 如果点击双人使用左键,调用callback1
self.c.bind("<Button-3>", self.callback2) # 如果点击双人使用右键,调用callback2
elif isinstance(event.widget, Button) and (event.widget["text"]) == "单人":
self.c.bind("<Button-1>", self.rand) # 如果左键点击单人,调用rand
# self.c.mainloop() # 调用单人游戏方法
self.destroy_buttons() # 点击之后进入游戏,因此需要销毁按钮
def callback1(self, event):
try: # 捕捉异常防止误触
global temp #使得temp可更改
if temp % 2 == 0: #为偶数
u, v = event.x, event.y # 获取鼠标点击的位置:从event中提取鼠标的x和y坐标,并分别赋值给u和v
temp += 1
else:
showinfo("ERROR", "现在是白方回合!")
# 确定鼠标点击的棋盘格子
for i in range(1, 16): # 用于确定x坐标(即u)对应的棋盘列索引zx
if 30 * i < u < 30 * (2 * i + 1):
zx = i - 1
break
for i in range(1, 16): # 用于确定y坐标(即v)对应的棋盘行索引zy
if 30 * i < v < 30 * (2 * i + 1):
zy = i - 1
break
self.zx = zx # 存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = 1 # 在棋盘上放黑子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20,
(zx + 1) * 60 + 20, (zy + 1) * 60 + 20, fill="black") # 绘制棋子
win = self.check_win(zx, zy) # 判断是否有玩家获胜,传入棋子的位置
if win == 1:
showinfo("Game over", "黑方获胜!")
temp += 1 #如果黑方获胜,那么黑方必定是最后一个落子,要控制黑方先手temp+=1
self.start(event) # 重置游戏状态
self.is_continue_game(event)
except UnboundLocalError as u:
print(u)
except Exception as e:
showwarning("Warning", "请在棋盘上落子!")
def callback2(self, event):
try: # 捕捉异常防止崩溃
global temp # 声明全局变量
if temp % 2 != 0: # 为奇数
u, v = event.x, event.y # 获取鼠标点击的位置:从event中提取鼠标的x和y坐标,并分别赋值给u和v
temp += 1
else:
showinfo("ERROR", "现在是黑方回合!")
# 确定鼠标点击的棋盘格子
for i in range(1, 16):
if 30 * i < u < 30 * (2 * i + 1):
zx = i - 1
break
for i in range(1, 16):
if 30 * i < v < 30 * (2 * i + 1):
zy = i - 1
break
self.zx = zx # 存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = -1 # 在棋盘上放白子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20,
(zx + 1) * 60 + 20, (zy + 1) * 60 + 20, fill="white") # 绘制椭圆并保存其ID
win = self.check_win(zx, zy) # 判断胜负
if win == -1:
showinfo("Game over", "白方获胜!")
self.start(event) # 重置游戏状态
self.is_continue_game(event)
except UnboundLocalError as u:
print(u)
except Exception as e:
showwarning("Warning", "请在棋盘上落子!")
print(e)
def rand(self, event):
u, v = event.x, event.y # 获取鼠标点击的位置
# 确定鼠标点击的棋盘格子
for i in range(1, 16):
if 30 * i < u < 30 * (2 * i + 1):
zx = i - 1
break
for i in range(1, 16):
if 30 * i < v < 30 * (2 * i + 1):
zy = i - 1
break
try: #防止用户误触 捕捉异常
self.zx = zx #存储棋子坐标,用于悔棋
self.zy = zy
if self.is_repeat(zx, zy): # 判断落子是否重复
self.matrix[zx][zy] = 1 # 在棋盘上放黑子
self.oval_id = self.c.create_oval((zx + 1) * 60 - 20, (zy + 1) * 60 - 20, (zx + 1) * 60 + 20, (zy + 1) * 60 + 20,
fill="black") # 绘制棋子
# 自动落白子:为了在给定的黑子周围找到一个空位置并自动放置一个白子
# 1.遍历黑子周围的8个格子将每一个空格子的坐标添加到s中,然后从s中随机选择一个坐标,并在图形界面上的对应位置画一个白色的圆形表示白子
# 2.为防止从s中选到了白子的位置,从而覆盖了白子,可以设置当上面没有落子的时候再绘画棋子,否则继续生成随机坐标
# 3.防止当黑子落在棋盘边缘的时候白子落在棋盘外面,如果i和j都大于0小于15(取15会落在外面),正常取值,否则如果小于0取反,大于15-2
# 4.黑子如果下在一个周围都有棋子的地方,会导致s是空列表,我们可以直接判断s是否为空,如果为空则找空随机落子,否则从s中选择落子
s = []
for i in range(zx - 1, zx + 2):
for j in range(zy - 1, zy + 2):
if self.matrix[i][j] == 0: # 如果没有落子
if 0 <= i <= 14 and 0 <= j <= 14:
L = (i, j)
else:
if i == -1:
i += 1
if j == -1:
j += 1
if i == 15:
i -= 2
if j == 15:
j -= 2
L = (i, j)
if L not in s:
s.append(L)
# 确保s不是空列表
if not s:
a, b = random.randint(0, 14), random.randint(0, 14)
while self.matrix[a][b] != 0: # 确保位置是空的
a, b = random.randint(0, 14), random.randint(0, 14)
wz = (a, b) # 生成随机坐标
else:
wz = random.choice(s) # 从s中生成随机坐标
# 为了防止自动生成的白子将原来的黑子覆盖,通过while循环判断随机生成的坐标上有无棋子,无则落子,有则重新生成直至落子成功
# 黑子如果下在一个周围都有棋子的地方,白旗没有地方放置一直生成位置,就会陷入死循环,因此我们可以使用一个count查看循环的次数
# 如果循环超过60次就在地图上随机找一个空位置放置棋子(并不是固定60,而是要给够while找到位置的时间)
count = 0
while True:
count += 1
if self.matrix[wz[0]][wz[1]] == 0:
self.oval_id = self.c.create_oval((wz[0] + 1) * 60 - 20, (wz[1] + 1) * 60 - 20,
(wz[0] + 1) * 60 + 20, (wz[1] + 1) * 60 + 20, fill="white")
self.matrix[wz[0]][wz[1]] = -1 # 将+设置随机坐标设置为白子
break
else:
wz = random.choice(s)
if count >= 60:
a, b = random.randint(0, 14), random.randint(0, 14)
if self.matrix[a][b] == 0:
self.oval_id = self.c.create_oval((a + 1) * 60 - 20, (b + 1) * 60 - 20,
(a + 1) * 60 + 20, (b + 1) * 60 + 20, fill="white")
self.matrix[a][b] = -1 # 将+设置随机坐标设置为白子
break
# 因为这里的wz[0], wz[1]和zx,zy是两个不同的参数,因此单独判断
win_white = self.check_win(wz[0], wz[1])
win_black = self.check_win(zx, zy) # 判断胜负
if win_white == -1:
showinfo("Game over", "白方获胜!")
self.start(event)
self.is_continue_game(event)
if win_black == 1:
showinfo("Game over", "黑方获胜!")
self.start(event) # 重置游戏状态
self.is_continue_game(event)
except Exception as e:
showwarning("Warning", "请在棋盘上落子!")
print(e)
def check_win(self, x, y): # 传入棋盘上的坐标x y
four_direction = [] # 初始化方向列表:存储四个方向(水平、垂直、左斜、右斜)的棋子序列
# 获取水平和垂直方向的列表:分别获取了(x, y)位置所在的行和列的棋子序列
four_direction.append([self.matrix[i][y] for i in range(15)]) # 水平
four_direction.append([self.matrix[x][j] for j in range(15)]) # 垂直
# 获取左斜方向的列表:即把左斜方向每个棋格的状态放进列表
zuox = []
for i in range(15): # 遍历棋盘找到左斜方向的棋子:根据规律可以得知,坐标之和相等放在同一组列表
for j in range(15): # self.matrix[i][j]获取格子的状态,0没有放,1黑子,-1白子
if i + j == x + y:
zuox.append(self.matrix[i][j]) # 每次下棋时,坐标之和相等放在同一组列表,若和不相等会重新创造列表
four_direction.append(zuox)
print("左斜为", zuox)
# 获取右斜方向的列表
youx = [] # 根据规律可以把坐标之差相等的棋子分为一组
if x < y: # 如果x < y,则沿着主对角线向右上方遍历,否则,沿着次对角线向右下方遍历。
for i in range(15 - (y - x)):
youx.append(self.matrix[i][i + (y - x)])
else:
for i in range(15 - (x - y)):
youx.append(self.matrix[i + (x - y)][i])
four_direction.append(youx)
print("右斜为", youx)
# 检查连续棋子
black, white = [0], [0]
for i in four_direction: # 检查每个方向上的连续棋子序列,统计每个方向上连续黑棋和白棋的数量
for key, group in itertools.groupby(i): # groupby()方法:把可迭代对象中相邻的重复元素挑出来放一起
if key == 1: # 在这里是将每个方向上相邻的重复棋子放在一起,查看哪个棋子的数量先达到5个
black.append(len(list(group)))
elif key == -1:
white.append(len(list(group)))
if max(black) >= 5:
return 1
elif max(white) >= 5:
return -1
# 当点击一个棋格的时候,他被赋值为1或-1,可以通过判断他有棋子来判断能否点击,如果不能点击,阻止赋值操作和绘画棋子并提示用户
# 如果坐标上面没有棋子,返回True可以进行赋值和绘画,反之返回False
def is_repeat(self, x, y):
global temp
if self.matrix[x][y] == 0:
return True
else:
temp -= 1 # 控制先后手
showwarning("ERROR!", "落子错误,请重新落子!")
return False
pass
# 销毁按钮,防止误触从而重新进入游戏界面
def destroy_buttons(self):
for button in self.buttons:
button.destroy() # 销毁每个按钮
self.buttons.clear() # 清空按钮列表,以防之后重复销毁导致崩溃
# 游戏结束后,判断用户是否继续游戏还是结束游戏
def is_continue_game(self, event):
answer = askquestion("Game over", "是否继续游戏?")
if answer == "yes":
self.start(event)
# 因为前面销毁了游戏模式按钮,因此需要重新选择游戏模式
answer1 = askyesno("选择模式", "Y(双人)/N(单人)")
if answer1:
self.start(event)
self.c.bind("<Button-1>", self.callback1) # 上面有讲解
self.c.bind("<Button-3>", self.callback2)
else:
self.start(event)
self.c.bind("<Button-1>", self.rand)
else:
self.root.destroy()
# 悔棋:在棋盘旁边添加一个悔棋按钮,点击一下,删除椭圆
# 放置棋子并保存坐标and绘制椭圆并保存其ID
# 使用if判断是否按钮被点击,如果点击 使用图形ID删除椭圆,并且通过坐标把原来的赋值更改为0
def regret_chess(self, event):
global temp
answer = askyesno("悔棋", "是否要悔棋?")
if answer:
temp -= 1 # 操控先后手
self.matrix[self.zx][self.zy] = 0
self.c.delete(self.oval_id)
else:
pass
GomokuGame()
图片素材
- main_page
- background