python五子棋全代码
前言
自2020年10月14日我首次公开完整的Java五子棋代码以来,时光已悄然流逝了近五个春秋。这期间,我从一名青涩的大学生蜕变成为拥有三年工作经验的全栈开发者,编程思维与技术水平均实现一定的提升。当我再次审视昔日亲手编写的Java五子棋代码时,不禁发现了诸多“屎山代码”,这些陈旧的代码宛如难以下咽的磐石,重写的难度无异于自我设障。于是,我一拍大腿,决定另起炉灶,这也正是本文诞生的契机。
近期,我的笔触多聚焦于Python编程领域,尽管我的职业生涯主要围绕Java与Vue等技术栈展开。长期使用Java后,我在业余时间渴望寻求一种更为轻松愉悦的编程体验,Python便自然而然地成为了我的兴趣之选。其简洁高效的特点,让我能够迅速将创意转化为现实,无需过多繁琐的编码过程。
接下来,就让我们一同见证我用Python重新打造的五子棋游戏吧。
效果图和功能
部分效果图,其余功能大家自行探索。
- 单机模式:两人面对面游玩。
- 人机模式:挑战若智人机。
- 联机模式:局域网联机。
- 重开、悔棋、认输等基础操作。
项目结构
- main.py —— 启动程序
- MainWindow.py —— 主窗口
- UserInterface.py —— 用户界面绘制
- ButtonFunction.py —— 按钮相关函数
- MouseFunction.py —— 鼠标相关函数
- ProduceResult.py —— 判断对局结果
- AIAlgorithm.py —— 人机对战
- OnlineFunction.py —— 联机对战
源代码
main.py —— 启动程序
from PyQt5.QtWidgets import QApplication
from MainWindow import MainWindow
def main():
app = QApplication([])
# 创建主窗口
MainWindow()
# 进入事件循环
app.exec_()
if __name__ == "__main__":
main()
MainWindow.py —— 主窗口
import socket
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication
from ButtonFunction import initButton, selectMode
from MouseFunction import containerMouseClicked, containerMouseMove
from UserInterface import initUI, drawChessboard
# 主窗口
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# 获取桌面尺寸
self.desktop = QApplication.desktop()
self.screen_rect = self.desktop.screenGeometry()
# 设置主窗口比例
self.main_width = int(self.screen_rect.width() * 0.45)
self.main_height = int(self.screen_rect.height() * 0.55)
# 设置主窗口位置
self.main_move_x = (self.screen_rect.width() - self.main_width) // 2
self.main_move_y = (self.screen_rect.height() - self.main_width) // 2
# 状态栏
self.status = self.statusBar()
# 整体容器
self.container = QWidget(self)
# 重写鼠标点击事件
self.container.mousePressEvent = self.containerMousePressEvent
# 开启鼠标跟踪
self.container.setMouseTracking(True)
# 重新鼠标移入事件
self.container.mouseMoveEvent = self.containerMouseMoveEvent
# 正方形棋盘位置大小
self.chessboard = 15
self.chessboard_size = int(self.main_height * 0.9)
self.chessboard_move = int(self.chessboard_size - self.chessboard_size * 0.95)
self.chessboard_size_move = self.chessboard_size + self.chessboard_move
# 正方形网格间距
self.grid_size = self.chessboard_size / (self.chessboard - 1)
# 按钮大小
self.button_width = int(self.main_width * 0.2)
self.button_height = int(self.main_height * 0.05)
# 按钮位置
self.button_x = self.chessboard_size_move + (
(self.main_width - self.chessboard_size_move) / 2 - self.button_width / 2)
self.button_y = self.main_height / 3
# 棋子坐标
self.chess_coord = []
# 棋子颜色
self.chess_color = True
# 棋子大小
self.chess_size = self.grid_size / 1.2
# 预要落子的坐标
self.advance_chess_coord = {}
# 游戏模式 0:单机 1:人机 2:联机
self.game_mode = 0
# 服务端IP
self.server_ip = ''
# 服务端口
self.server_port = 8080
# TCP/IP服务器
self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# TCP/IP的socket
self.tcp_socket = None
# 在线对战 棋手 颜色
self.chess_color_online = None
self.player_ready = False
# 初始化UI
initUI(self)
# 初始化按钮
initButton(self)
self.show()
# 选择游戏模式
selectMode(self)
# 重写绘图事件
def paintEvent(self, event):
drawChessboard(self)
# 重写container的鼠标点击事件
def containerMousePressEvent(self, event):
containerMouseClicked(self, event)
# 重写container的鼠标移入事件
def containerMouseMoveEvent(self, event):
containerMouseMove(self, event)
UserInterface.py —— 用户界面绘制
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPen, QColor, QBrush, QPainter
# 初始化UI
def initUI(self):
# 主窗口大小
self.resize(self.main_width, self.main_height)
# 固定主窗口大小
self.setFixedSize(self.width(), self.height())
# 主窗口居中
self.move(self.main_move_x, self.main_move_y)
# 状态栏和标题
self.status.showMessage('请开始游戏')
self.setWindowTitle('五子棋')
# 创建容器
self.setCentralWidget(self.container)
# 绘制棋盘
def drawChessboard(self):
painter = QPainter(self)
# 使用抗锯齿
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.HighQualityAntialiasing)
# 设置画笔
painter.setPen(QPen(QColor("black")))
# 设置画刷
painter.setBrush(QBrush(QColor("black")))
# 绘制方格线
for i in range(15):
# 设置线的粗细
pen = QPen(QColor("black"), 2) if i == 3 or i == 11 else QPen(QColor("black"), 1)
painter.setPen(pen)
# 纵向线
painter.drawLine(self.grid_size * i + self.chessboard_move,
self.chessboard_move,
self.grid_size * i + self.chessboard_move,
self.chessboard_size_move)
# 横向线
painter.drawLine(self.chessboard_move,
self.grid_size * i + self.chessboard_move,
self.chessboard_size_move,
self.grid_size * i + self.chessboard_move)
# 绘制四个点
dot_move1 = self.grid_size * 3
dot_move2 = self.grid_size * 11
dot_move3 = self.grid_size * 7
dot_size = 10
points = [(dot_move1, dot_move1),
(dot_move2, dot_move1),
(dot_move2, dot_move2),
(dot_move1, dot_move2),
(dot_move3, dot_move3)]
for x, y in points:
x += self.chessboard_move - dot_size / 2
y += self.chessboard_move - dot_size / 2
painter.drawEllipse(x, y, dot_size, dot_size)
# 绘制预落子
if self.advance_chess_coord:
drawChess(self, painter, self.advance_chess_coord)
# 绘制棋子
for coord in self.chess_coord:
drawChess(self, painter, coord)
# 绘制棋子
def drawChess(self, painter, coord):
# 普通棋子
brush = QBrush(QColor("black")) if coord.get('color', True) else QBrush(QColor("white"))
# 胜利后棋子
pen = QPen(QColor("yellow"), 2) if coord.get('outline', False) else QPen(QColor("black"), 1)
# 预落下棋子
if coord.get('calculate', False):
brush = QBrush(QColor(0, 0, 0, 20))
pen.setStyle(Qt.DashLine)
painter.setBrush(brush)
painter.setPen(pen)
painter.drawEllipse(coord['x'] * self.grid_size + self.chessboard_move - self.chess_size / 2,
coord['y'] * self.grid_size + self.chessboard_move - self.chess_size / 2,
self.chess_size,
self.chess_size)
ButtonFunction.py —— 按钮相关函数
from functools import partial
from PyQt5.QtWidgets import QPushButton, QMessageBox
from AIAlgorithm import selectSquads
from OnlineFunction import selectSide
# 初始化按钮
def initButton(self):
buttons = [
{
'text': '选择模式',
'clicked': partial(selectMode, self)
},
{
'text': '重新开始',
'clicked': partial(startGame, self)
},
{
'text': '悔棋',
'clicked': partial(regret, self)
},
{
'text': '认输',
'clicked': partial(giveUp, self)
}]
for i, info in enumerate(buttons):
button = QPushButton(self.container)
button.setText(info['text'])
button.setGeometry(self.button_x, self.button_y + self.button_height * i, self.button_width,
self.button_height)
button.clicked.connect(partial(info['clicked']))
# 选择模式
def selectMode(self):
msg = QMessageBox(QMessageBox.Question, "选择", "请选择游戏模式")
msg.addButton(self.tr("单机"), QMessageBox.AcceptRole)
msg.addButton(self.tr("人机"), QMessageBox.AcceptRole)
msg.addButton(self.tr("联机"), QMessageBox.AcceptRole)
exit_button = msg.addButton(self.tr("退出"), QMessageBox.AcceptRole)
self.game_mode = msg.exec_()
if msg.clickedButton() == exit_button:
exit()
startGame(self)
if self.game_mode == 2:
selectSide(self)
# 开始游戏
def startGame(self):
self.chess_coord = []
self.chess_color = True
self.status.showMessage("等待黑方落子")
self.update()
if self.game_mode == 1:
selectSquads(self)
# 悔棋
def regret(self):
if len(self.chess_coord) > 0:
self.chess_coord.pop()
if self.game_mode == 1 and len(self.chess_coord) > 0:
self.chess_coord.pop()
self.update()
# 认输
def giveUp(self):
QMessageBox.information(self, "游戏结束",
f"{'黑方' if self.chess_color else '白方'}认输,恭喜{'黑方' if not self.chess_color else '白方'}胜利!",
QMessageBox.Yes, QMessageBox.Yes)
startGame(self)
MouseFunction.py —— 鼠标相关函数
import json
import numpy as np
from AIAlgorithm import aiGame
from ProduceResult import checkWin
# 计算棋子坐标
def calculateCoord(self, event):
x = event.pos().x()
y = event.pos().y()
# 限制点击区域
if x < self.chessboard_move - 10 or \
x > self.chessboard_size_move + 10 or \
y < self.chessboard_move - 10 \
or y > self.chessboard_size_move + 10:
return False
# 计算落子位置
x = np.round((x - self.chessboard_move) / self.grid_size)
y = np.round((y - self.chessboard_move) / self.grid_size)
# 判断位置上是否已有棋子
if any(coord['x'] == x and coord['y'] == y for coord in self.chess_coord):
return False
return {'x': x, 'y': y, 'color': self.chess_color}
# 重写后的container鼠标点击事件
def containerMouseClicked(self, event):
if self.game_mode == 2 and not self.player_ready:
return
if self.chess_color != self.chess_color_online and self.chess_color_online is not None:
return
coord = calculateCoord(self, event)
if coord:
# 将棋子坐标添加到列表中
self.chess_coord.append(coord)
if self.game_mode == 2:
self.tcp_socket.sendall(json.dumps(coord).encode('utf-8'))
render(self)
# 渲染当前棋子布局
def render(self):
# 重绘
self.update()
# 判断是否胜利
if checkWin(self):
return
# 切换颜色
self.chess_color = not self.chess_color
self.status.showMessage(f"等待{'黑方' if self.chess_color else '白方'}落子")
# 判断是否是人机模式
if self.game_mode == 1:
# AI行动
aiGame(self)
# 判断是否胜利
if checkWin(self):
return
# 重绘
self.update()
# 重写后的container鼠标移动事件
def containerMouseMove(self, event):
if self.game_mode == 2 and not self.player_ready:
return
if self.chess_color != self.chess_color_online and self.chess_color_online is not None:
self.advance_chess_coord = None
self.update()
return
coord = calculateCoord(self, event)
if coord:
coord['calculate'] = True
# 将棋子坐标添加到列表中
self.advance_chess_coord = coord
self.update()
ProduceResult.py —— 判断对局结果
from itertools import product
from PyQt5.QtWidgets import QMessageBox
from ButtonFunction import startGame
# 判断是否胜利
def checkWin(self):
# 判断是否五子连珠
linking_coords = checkLink(self)
if linking_coords:
color = linking_coords[-1]['color']
QMessageBox.information(self, "游戏结束", f"恭喜{'黑方' if color else '白方'}胜利!",
QMessageBox.Yes, QMessageBox.Yes)
startGame(self)
return True
# 判断棋盘空间是否已满
if len(self.chess_coord) == 225:
QMessageBox.information(self, "游戏结束", f"棋盘已满,平局结束!",
QMessageBox.Yes, QMessageBox.Yes)
startGame(self)
return True
return False
# 计算连接数
def checkLink(self):
if not self.chess_coord:
return False
last_coord = self.chess_coord[-1]
directions = [
(1, 0), # 垂直方向
(0, 1), # 水平方向
(1, 1), # 右对角线
(-1, 1) # 左对角线
]
for direction, method in product(directions, range(5)):
linking_coords = []
for i in range(5):
new_coord = {
'x': last_coord['x'] + direction[0] * (i - method),
'y': last_coord['y'] + direction[1] * (i - method),
'color': last_coord['color']
}
if new_coord in self.chess_coord:
new_coord['outline'] = True
linking_coords.append(new_coord)
if len(linking_coords) == 5:
self.chess_coord.extend(linking_coords)
self.update()
return linking_coords
return False
AIAlgorithm.py —— 人机对战
from random import choice
import numpy as np
from PyQt5.QtWidgets import QMessageBox
# 选择阵容
def selectSquads(self):
msg_squads = QMessageBox(QMessageBox.Question, "选择", "请选择您的阵容")
msg_squads.addButton(self.tr("黑子"), QMessageBox.AcceptRole)
white = msg_squads.addButton(self.tr("白子"), QMessageBox.AcceptRole)
msg_squads.exec_()
# 玩家选择白方,则人机执黑先行
if msg_squads.clickedButton() == white:
aiGame(self)
# 人机对战AI算法
def aiGame(self):
ai_coord = strategy(self)
self.chess_coord.append({'x': ai_coord[0], 'y': ai_coord[1], 'color': self.chess_color})
# 切换颜色
self.chess_color = not self.chess_color
self.status.showMessage(f"等待{'黑方' if self.chess_color else '白方'}落子")
# 开始计算
def strategy(self):
# 将黑白坐标拆分 ([白方坐标],[黑方坐标])
board = ([], [])
for coord in self.chess_coord:
if coord['color']:
board[0].append((coord['x'], coord['y']))
else:
board[1].append((coord['x'], coord['y']))
# 将坐标转换成二维数组 1: 我方 -1: 敌方 0: 空位
table = np.zeros([self.chessboard, self.chessboard])
for i in range(self.chessboard):
for j in range(self.chessboard):
if self.chess_color:
if (i, j) in board[0]:
table[i, j] = -1
elif (i, j) in board[1]:
table[i, j] = 1
else:
if (i, j) in board[0]:
table[i, j] = 1
elif (i, j) in board[1]:
table[i, j] = -1
# 判断自己是否先手
if len(board[0]) == 0 and len(board[1]) == 0:
return 7, 7
else:
score_table = {}
for i in range(self.chessboard):
for j in range(self.chessboard):
if table[i, j] == 0:
table[i, j] = 1
score_table[(i, j)] = heuristic(self, table)
table[i, j] = 0
self_position = randomChoose(score_table)
return self_position[0], self_position[1]
# 计分规则
def score(five_tuple):
if len(five_tuple) != 5:
print("ERROR")
return None
if 1 in five_tuple and -1 in five_tuple:
return 0
elif sum(five_tuple) == 0:
return 7
elif sum(five_tuple) == -1:
return -35
elif sum(five_tuple) == -2:
return -800
elif sum(five_tuple) == -3:
return -15000
elif sum(five_tuple) == -4:
return -800000
elif sum(five_tuple) == -5:
return -10000000
elif sum(five_tuple) == 5:
return 10000000
elif sum(five_tuple) == 1:
return 15
elif sum(five_tuple) == 2:
return 400
elif sum(five_tuple) == 3:
return 1800
elif sum(five_tuple) == 4:
return 100000
# 计分
def heuristic(self, table):
sum_score = 0
for i in range(self.chessboard):
for j in range(self.chessboard):
if j + 4 < self.chessboard:
sum_score += score(tuple(table[i, j:j + 5]))
if i + 4 < self.chessboard:
sum_score += score(tuple(table[i:i + 5, j]))
if i + 4 < self.chessboard and j + 4 < self.chessboard:
five_tuple = []
for k in range(5):
five_tuple.append(table[i + k, j + k])
sum_score += score(tuple(five_tuple))
if i + 4 < self.chessboard and j - 4 >= 0:
five_tuple = []
for k in range(5):
five_tuple.append(table[i + k, j - k])
sum_score += score(tuple(five_tuple))
return sum_score
# 选择最高分位置
def randomChoose(score_table):
max_value = max(score_table.items(), key=lambda x: x[1])[1]
positions = []
for item in score_table.items():
if item[1] == max_value:
positions.append(item[0])
return choice(positions)
OnlineFunction.py —— 联机对战
import json
import socket
import threading
from functools import partial
import pyperclip
from PyQt5.QtWidgets import QMessageBox, QInputDialog
# 选择服务端或客户端
def selectSide(self):
msg_side = QMessageBox(QMessageBox.Question, "选择", "请选择创建或进入房间")
server_button = msg_side.addButton(self.tr("创建房间"), QMessageBox.AcceptRole)
client_button = msg_side.addButton(self.tr("进入房间"), QMessageBox.AcceptRole)
msg_side.exec_()
if msg_side.clickedButton() == server_button:
checkNetwork(self)
extractIp(self)
server(self)
elif msg_side.clickedButton() == client_button:
checkNetwork(self)
client(self)
# 检查网络连接
def checkNetwork(self):
try:
# 超时时间
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(('www.baidu.com', 80))
except socket.error as ex:
QMessageBox.critical(self, "网络连接异常", "当前网络不可用,请检查您的网络状态\n" + str(ex))
selectSide(self)
# 获取IP地址
def extractIp(self):
st = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
st.connect(('10.255.255.255', 1))
self.server_ip = st.getsockname()[0]
pyperclip.copy(self.server_ip)
QMessageBox.information(self, "房间信息", "IP:" + self.server_ip + ",已复制到剪贴板")
# 服务端
def server(self):
# 绑定IP端口
self.tcp_server.bind((self.server_ip, self.server_port))
# 可设置最大连接数
self.tcp_server.listen()
threading.Thread(target=partial(startServerListen, self)).start()
# 服务端监听线程
def startServerListen(self):
while True:
try:
self.status.showMessage('房间创建成功, 等待对方加入...')
self.tcp_socket, address = self.tcp_server.accept()
threading.Thread(target=partial(receiveData, self)).start()
self.player_ready = True
self.chess_color_online = True
self.status.showMessage('对方已加入, 可以开始游戏')
except:
break
# 客户端
def client(self):
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
text, ok = QInputDialog.getText(None, "输入框", "请输入房间IP:")
if ok and text != '':
print("你输入的内容是:", text)
self.server_ip = text
else:
print("用户取消了输入或没有输入内容")
return selectSide(self)
try:
self.tcp_socket.connect((self.server_ip, self.server_port))
except socket.error as e:
QMessageBox.critical(self, "网络连接异常", "无法连接到服务器,请检查IP地址是否正确\n" + str(e))
selectSide(self)
threading.Thread(target=partial(receiveData, self)).start()
self.player_ready = True
self.chess_color_online = False
self.status.showMessage('您已经加入房间, 请等待房主开始游戏')
# 接收数据
def receiveData(self):
from MouseFunction import render
while True:
try:
data = self.tcp_socket.recv(1024)
if data:
self.chess_coord.append(json.loads(data.decode('utf-8')))
else:
break
except socket.timeout:
print("接收数据超时,正在重试...")
continue
except Exception as e:
print(f"接收数据时发生错误:{e}")
break
render(self)
结束语
随着这篇文章的收尾,我的Python版五子棋项目也暂告一段落。虽然项目不大,但它不仅是对我编程技能的一次小小挑战,更是对过往编程经历的一次深刻反思与成长。从Java到Python,不仅是编程语言的转换,更是编程思维与习惯的革新。未来,我将继续在这条编程之路上探索前行,不断学习新知识,挑战新高度,用代码编织出更加丰富多彩的数字世界。感谢每一位读者的陪伴与支持,期待与你们在编程的海洋里再次相遇,共同书写属于我们的精彩篇章。
同学们按照目录复制到对应的文件中,运行main即可启动,如果不想复制,或复制后无法运行,可以私聊我获取项目仓库地址,以拉取完整项目。
联机模式目前只完成了棋子的同步,后续功能将更新在仓库中。