【Python】Tkinter制作简单五子棋小游戏


一、效果预览

  • 主页面主页面
  • 游戏页面
    游戏页面

二、预处理

  • 导入模块
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 = zxself.zy = zy存储棋子坐标
      • 删除棋子:在绘制棋子的时候保存其IDself.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
    main_page
  • background
    background
下面是一个简单五子棋 Python 代码实现,你可以在 Python 环境下运行它: ```python import numpy as np class Game: def __init__(self, board_size=15): self.board_size = board_size self.board = np.zeros((board_size, board_size)) self.players = [1, 2] self.num_moves = 0 def get_valid_moves(self): return np.argwhere(self.board == 0) def make_move(self, move, player): self.board[move[0], move[1]] = player self.num_moves += 1 def check_win(self, player): for i in range(self.board_size): for j in range(self.board_size): if self.board[i][j] == player: if self.check_direction(i, j, player, (0, 1)) or \ self.check_direction(i, j, player, (1, 0)) or \ self.check_direction(i, j, player, (1, 1)) or \ self.check_direction(i, j, player, (1, -1)): return True return False def check_direction(self, i, j, player, direction): count = 0 while i >= 0 and i < self.board_size and j >= 0 and j < self.board_size and \ self.board[i][j] == player and count < 5: count += 1 i += direction[0] j += direction[1] if count == 5: return True else: return False def play(self): current_player = self.players[self.num_moves % 2] while True: valid_moves = self.get_valid_moves() if len(valid_moves) == 0: print("Game over: tie") return move = tuple(map(int, input("Player {}, enter your move (row, col): ".format(current_player)).split(","))) if move not in valid_moves: print("Invalid move, try again.") continue self.make_move(move, current_player) if self.check_win(current_player): print("Player {} wins!".format(current_player)) return current_player = self.players[self.num_moves % 2] game = Game() game.play() ``` 这个代码实现了一个基本的五子棋游戏,玩家可以交替输入落子的位置,程序会检查落子的合法性以及是否有玩家胜出。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>