智能五子棋游戏
设计要求
使用Python语言,结合博弈树启发式搜索和alpha-beta剪枝技术,开发一个人机五子棋博弈游戏
具体要点
- 设计一个15行15列棋盘,要求自行给出估价函数,按极大极小搜索方法,并采用α-β剪枝技术;
- 采用人机对弈方式,对弈双方设置不同颜色的棋子,一方走完一步后,等待对方走步,对弈过程的每个棋局都在屏幕上显示出来;
- 当某一方在横、竖或斜方向上先有5个棋子连成一线时,该方为赢。
PS
本人能力有限😢,游戏以玩家执黑先下,且忽略禁手为前提进行设计。
代码部分
-
导入包及项目初始化
引用tkinter库进行GUI图形界面的创建,并且引用tkinter.messagebox制作游戏结束后显示结果的窗格。
导入numpy库用于各项运算,便于后续实现剪枝和搜索等功能。
导入time库用于记录时间,便于后续记录AI思考时间。
定义棋盘大小为15乘15,满足实验要求。
区分棋盘状态,分空、黑子、白子三种情况。根据当前下棋者不同区分玩家轮和AI轮,并默认玩家执黑棋,AI执白棋。
根据五子棋中“活三”、“死四”等不同棋形赋不同权重,便于后续评估当前棋局得分。
依次定义博弈树的搜索深度和搜索方向,并初始化棋盘为15乘15的全0矩阵。
根据五子棋中较复杂的连珠情况给出权重,从而使AI对局势有更清晰的判断。
"""
-*- coding: utf-8 -*-
Desc: 智能五子棋
Auth: RyanZzzzq
GitHub:RyanZzzzq
Intro: 使用Python语言,结合博弈树启发式搜索和alpha-beta剪枝技术,开发一个五子棋博弈游戏。
"""
import tkinter as tk
import tkinter.messagebox
import numpy as np
# 棋盘大小
BOARD_SIZE = 15
# 定义棋盘状态
EMPTY = 0
BLACK = 1
WHITE = 2
# 定义评估函数中的权重
# 根据五子棋中连珠情况,给出权重
WEIGHTS = {
"open_two": 10, # 活二
"half_three": 100, # 死三
"open_three": 1000, # 活三
"half_four": 10000, # 死四
"open_four": 100000, # 活四
"five": 1000000 # 五连
}
# 定义搜索深度
MAX_DEPTH = 3
# 定义搜索方向,包括水平、垂直和对角线
DIRECTIONS = [(1, 0), (0, 1), (1, 1), (1, -1)]
# 初始化棋盘
board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
# 用户执黑,AI执白
PLAYER_COLOR = BLACK
AI_COLOR = WHITE
# 定义棋局状态
PLAYER_ROUND = 1
AI_ROUND = 2
-
定义GUI界面参数
创建棋盘后,依次定义GUI界面中棋盘的边距、格子大小。
绘制棋盘格线,便于观察落子位置。
定义棋子半径。
# 定义棋子的半径
RADIUS = 15
# 定义棋盘的边距
PADDING = 20
# 定义棋盘格子的大小
GRID_SIZE = 30
# 创建窗口
window = tk.Tk()
window.title('五子棋')
# 创建棋盘
canvas = tk.Canvas(window, width=PADDING*2+GRID_SIZE*(BOARD_SIZE-1), height=PADDING*2+GRID_SIZE*(BOARD_SIZE-1),
bg="#CDBA96")
canvas.pack()
# 绘制棋盘格线
for board_i in range(BOARD_SIZE):
canvas.create_line(PADDING, PADDING + board_i * GRID_SIZE, PADDING + (BOARD_SIZE - 1) * GRID_SIZE,
PADDING + board_i * GRID_SIZE)
canvas.create_line(PADDING + board_i * GRID_SIZE, PADDING, PADDING + board_i * GRID_SIZE,
PADDING + (BOARD_SIZE - 1) * GRID_SIZE)
-
获取合法移动位置函数
获取当前棋局合法移动位置,仅在已落子附近搜索可行位置,从而缩短AI思考时间,平均时长40s,基本保持AI智能程度不变,对弈难度适中。
def get_valid_moves(game_board):
"""
获取当前棋局的合法移动位置
"""
moves = set()
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
if game_board[i][j] != EMPTY:
# 检查此棋子周围的位置
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
ni, nj = i + di, j + dj
if 0 <= ni < BOARD_SIZE and 0 <= nj < BOARD_SIZE and game_board[ni][nj] == EMPTY:
moves.add((ni, nj))
return list(moves)
-
评估棋局得分函数
统计从水平、垂直和对角方向检查连子情况,通过新权重获得棋局得分。
def evaluate_position(game_board, color):
"""
评估当前棋局的得分
"""
score = 0
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
if game_board[i][j] == color:
for dx, dy in DIRECTIONS:
if 0 <= i + 4 * dx < BOARD_SIZE and 0 <= j + 4 * dy < BOARD_SIZE:
line = [game_board[i + k * dx][j + k * dy] for k in range(5)]
stone_count = sum(1 for p in line if p == color)
if stone_count == 5:
score += WEIGHTS["five"]
elif stone_count == 4 and EMPTY in line:
score += WEIGHTS["open_four"]
elif stone_count == 3 and line.count(EMPTY) == 2:
score += WEIGHTS["open_three"]
elif stone_count == 2 and line.count(EMPTY) == 3:
score += WEIGHTS["open_two"]
return score
-
判断游戏结束函数
根据某一方的棋局得分超过“五连”对应分数,即某个方向连子数量等于或超过5,判断游戏结束。
def is_game_over(game_board):
"""
检查游戏是否结束,即是否有一方获胜
返回获胜方的颜色,如果没有人获胜则返回 None
"""
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
if game_board[i][j] != EMPTY:
for direction in DIRECTIONS:
dx, dy = direction
x, y = i, j
count = 0
while 0 <= x < BOARD_SIZE and 0 <= y < BOARD_SIZE:
if game_board[x][y] == game_board[i][j]:
count += 1
if count == 5:
return game_board[i][j]
else:
count = 0
x += dx
y += dy
return None
-
α-β剪枝算法
Alpha-Beta剪枝是一种搜索算法,用以减少极小化极大算法(Minimax算法)搜索树的节点数。裁剪搜索树中没有意义的不需要搜索的树枝,提高运算速度。
如图所示,方框节点为AI回合,圆框节点为人回合。比如C节点,它需要从E和F当中选取最大的值。目前已经得出E节点的值为2,当搜索F节点时,由于F是人走的节点,则F需要从K、L、M中选取值最小的。因为K已经是1,也就是说F<=1,对该节点而言,α>=β,发生了α剪枝,则L,M不需要搜索。
轮到A节点,人的回合,需要从C和D中选取最小值,因为C节点的α值为2,而G是7,那么D至少是7。因此,D的剩余节点不需要检索,发生β剪枝。总结上面规律,我们可以得到剪枝方法如下:
(1). 当前为AI下棋节点:- α剪枝:如果当前节点的值不比父节点的前兄弟节点的大值大,则舍弃此节点。
- β剪枝:如果当前节点子节点的值不比当前节点的前兄弟节点中的最小值小,则舍弃该子节点和该子节点的所有后兄弟节点。
(2). 当前为用户下棋节点:
- α剪枝:如果当前节点的某子节点的值不比当前节点的前兄弟节点中的最大值大,则舍弃该子节点和该子节点的所有后兄弟节点。
- β剪枝:如果当前节点的子节点的值不比当前的父节点的前兄弟节点中的最小值小则舍弃此节点。
经过α-β剪枝,可以极大减少搜索的数量,提高检索效率。在代码中,根据剪枝算法的原理编写,实现检索过程中的剪枝。
def alpha_beta_search(game_board, depth, alpha, beta, maximizing_player):
"""
使用Alpha-Beta剪枝进行博弈树搜索
"""
if depth == 0 or is_game_over(game_board):
return evaluate_position(game_board, AI_COLOR) - evaluate_position(game_board, PLAYER_COLOR)
valid_moves = get_valid_moves(game_board)
if maximizing_player:
max_eval = float('-inf')
for move in valid_moves:
i, j = move
game_board[i][j] = AI_COLOR
evaluation = alpha_beta_search(game_board, depth - 1, alpha, beta, False)
game_board[i][j] = EMPTY
max_eval = max(max_eval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
break
return max_eval
else:
min_eval = float('inf')
for move in valid_moves:
i, j = move
game_board[i][j] = PLAYER_COLOR
evaluation = alpha_beta_search(game_board, depth - 1, alpha, beta, True)
game_board[i][j] = EMPTY
min_eval = min(min_eval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
break
return min_eval
-
AI移动函数
AI使用Alpha-Beta剪枝搜索选择最佳落子位置,进行移动。
对AI的移动进行调整,将移动在图形窗格中显示出来。
def make_ai_move():
"""
AI进行移动,使用Alpha-Beta剪枝搜索选择最佳位置
"""
window.update_idletasks()
best_score = float('-inf')
best_move = None
for move in get_valid_moves(board):
i, j = move
board[i][j] = AI_COLOR
score = alpha_beta_search(board, MAX_DEPTH, float('-inf'), float('inf'), False)
board[i][j] = EMPTY
if score > best_score:
best_score = score
best_move = move
if best_move:
i, j = best_move
board[i][j] = AI_COLOR
canvas.create_oval(PADDING+j*GRID_SIZE-RADIUS, PADDING+i*GRID_SIZE-RADIUS,
PADDING+j*GRID_SIZE+RADIUS, PADDING+i*GRID_SIZE+RADIUS, fill="white")
window.update()
-
鼠标点击事件函数
鼠标点击落子位置后,进行传值,选择到棋盘对应位置,并将棋子显示出来。
游戏结束后,根据结果不同弹出不一样的窗格。
def click(event):
"""
鼠标点击事件
"""
global board
i, j = round((event.y - PADDING) / GRID_SIZE), round((event.x - PADDING) / GRID_SIZE)
if 0 <= i < BOARD_SIZE and 0 <= j < BOARD_SIZE and board[i][j] == EMPTY:
board[i][j] = PLAYER_COLOR
canvas.create_oval(PADDING+j*GRID_SIZE-RADIUS, PADDING+i*GRID_SIZE-RADIUS, PADDING+j*GRID_SIZE+RADIUS,
PADDING+i*GRID_SIZE+RADIUS, fill="black")
if is_game_over(board):
tk.messagebox.showinfo("游戏结束", "你赢了!") # 根据游戏结果用户获胜显示对应信息
window.quit()
else:
make_ai_move()
if is_game_over(board):
tk.messagebox.showinfo("游戏结束", "AI赢了!") # 根据游戏结果AI获胜显示对应信息
window.quit()
-
绑定鼠标点击事件
将鼠标点击左键的动作和之前定义的鼠标点击事件绑定在一起。
# 绑定鼠标点击事件
canvas.bind("<Button-1>", click)
-
主循环入口
在主函数中开始主循环。
if __name__ == "__main__":
window.mainloop() # 进入主循环
运行结果
- 增加GUI图像界面显著提升游戏体验,用户选择落子位置便利程度大大提升。
- 与一般五子棋游戏体验十分接近,但是后期AI落子速度太慢,等待时间长,体验差。
参考文献
- 《理解Alpha-Beta剪枝算法》https://blog.csdn.net/qq_36612242/article/details/106425436;
- 《十四步实现拥有强大Al的五子棋游戏》https://www.cnblogs.com/goodness/archive/2010/05/27/1745756.html;