Python实现支持人机对战的五子棋软件(超详细)

利用pygame实现一个支持双人对战以及人机对战的小游戏

最终效果展示

在这里插入图片描述

总体框架介绍

windows.py负责处理素材图片以及将图片导入pygame。menu.py负责各级菜单的具体功能实现,包括菜单的绘制,点击效果,鼠标移动效果,点击返回等等。easypc.py是实现简单人机对战的主要程序,我采用了最简单的打分表,没有录入棋谱,也没有用博弈论决策树alphabeta剪枝,但是同样达到了较好的效果。judgewin.py主要实现对终局棋盘胜负判断。因为python无法直接实现跨文件的全局变量传输,(global 关键字可以定义一个变量为全局变量,但是这个仅限于在一个模块(py文件)中调用全局变量,在另外一个py文件 再次使用 global x 也是无法访问到的)所以我专门定义了一个全局变量管理模块gloval.py来管理全局变量。
在这里插入图片描述

具体功能以及算法思想

一、主界面与棋盘设计

  1. 采用了图形化的界面,一共有四张图片组成,分别是背景图片,棋盘图片,黑子图片,白字图片,菜单为绘图函数绘制。
  2. 在上部可以看到程序和作者信息:五子棋 by Ace Cheney。
  3. 当鼠标碰到菜单时,鼠标图标会由指针变成准星,同时方框以及字体变黑。
  4. 当点击游戏说明时,会出现游戏的相关介绍。
  5. 当点击开始游戏时,会进入二级菜单。
  6. 当点击双人人对战时,会进入三级菜单。此时鼠标移动到棋盘,棋子会显示出来。
  7. 当点击人机对战时,会进入四级菜单,让玩家选择是否先手。
  8. 当点击悔棋,产生悔棋效果。
  9. 当点击返回上级菜单时,返回上级菜单。
  10. 当落子后,会在每个落子上显示棋子的序号,且在最后一个棋子上面显示方框。

二、移位与胜负判定

  • 对于移位操作,首先要判断棋子是佛在棋盘内,如果在棋盘内,则显示棋子,如果不在棋盘内,则不显示棋子。
  • 对于在棋盘内的情况,需要先找到每个横纵交叉点的位置,即相对于棋盘和游戏界面的横纵坐标。
  • 之后利用round函数四舍五入鼠标点击的坐标值,取整且转换后的结果作为落子的坐标
  • 判断此落子位置是否为空,如果不为空则说明已经有落子,不能继续落子。
  • 对于胜负判定。分成三种情况,和棋,白棋获胜,黑棋获胜。
  • 当棋盘上每个交叉点都有落子,且不满足白棋获胜或者黑棋获胜,则判为和棋。
  • 当横向,纵向,左斜,右斜有五个白子或者五个黑子,则判为白子胜,或者黑子胜。

三、棋型价值设计

在实现人机对战功能时,核心思想是电脑计算出棋盘上每个格子的分数,之后选择分数最高的格子落子,这也就意味着需要通过一张打分表来进行评分。打分规则具体设计如下。

  • 一个交叉点视作一个位置,五个连续的位置视为一个组。
  • 计算每个组的分数,做累加,就得到总体得分。
  • 当没有棋子时,该位置得到7分。
  • 当有一个己方棋子时,该位置得到35分
  • 当有两个己方棋子时,该位置得到800分
  • 当有三个己方棋子时,该位置得到15000分
  • 当有四个己方棋子时,该位置得到800000分
  • 当有一个对方棋子时,该位置得到15分
  • 当有两个对方棋子时,该位置得到400分
  • 当有三个对方棋子时,该位置得到8000分
  • 当有四个对方棋子时,该位置得到100000分
  • 当双方棋子都在棋盘存在时,该位置得0分。
  • 在此基础上,遍历整个棋盘,找到整个棋盘上评分最高,次高,第三高的位置。
  • 在分数误差50以内,在最高,次高,第三高内随机选择一个位置落子。
  • 在分数误差100以内,在最高,次高内随机选择一个位置落子。
    最后三条是为了在玩家进行落子的时候走出不同的落子,以增加游戏性。

四、人机模式和双人模式的设计

  • 在主菜单中设计人机模式和双人模式菜单
  • 在双人模式中,黑方先落子,之后白方再落子,此视为一个回合。
  • 在人机模式中,分为两个子菜单。分别是电脑先手,和玩家先手。
  • 如果是电脑先手,则默认下在棋盘的最中央,载入相关信息后,按照人落子,电脑落子的先后顺序,视为一个回合。
  • 如果是人先手,则在人落子后,计算棋盘上空余格点的分数,之后按照相应要求落子。

五、游戏状态

在不同游戏状态下进行相同的操作可能会产生不同的结果,所以处理事件响应时要先判断游戏状态,再作出相应的反馈,这里我把游戏状态一共分成七类。

  • 1为主菜单状态
  • 2为人机和双人对战选择状态
  • 3为人机对战先后手选择状态
  • 4为双人对战状态
  • 5为人机对战,而且人先手状态
  • 6位人机对战,而且电脑先手状态
  • 7为游戏结束状态,即当一方赢棋或者和棋的状态

代码详解

wuziqi. py (核心模块)

主函数

  • 首先需要导入pygame,sys,numpy模块,以及自己的模块,在主函数中主要完成三件事,分别是素材的预处理和pygame的初始化,这两步都在自己创建的windows模块中
  • main()函数放在循环中,因为每次重新开始游戏,都会重新执行一次main函数。
#--coding:utf-8--
import pygame as pg
from sys import exit #用exit来退出程序
import windows as wd
import menu
import gloval 
import numpy
import judgewin
import easy_pc
gloval.init()
def main():
	wd.Image_PreProcessing()
	wd.windows()
	mainloop()
if __name__=='__main__':
	while 1:
		main()
		

程序主循环 : mainloop()

  • while restart == 0之前,是进行各种参数的初始化。将在用到每个参数时详细说明,这里不过多赘述,下面介绍循环内的详细过程

  • gamestate是游戏状态的标识符,默认为1。1为主菜单状态,2为人机和双人对战选择菜单状态,3为人机对战先后手选择菜单,4,5,6为对战菜单,7为结束游戏菜单,以下图为例,这是对战菜单状态,也就是说gamestate==4或者5,6,在这种状态下才会显示“悔棋”菜单,“重新开始”菜单,而且当光标移到棋盘上时,会将光标变成棋子的图标,而在其他状态下kennel不会显示“悔棋”菜单和“重新开始”菜单,而且当光标移动到棋盘上时,外观不会发生改变。
    在这里插入图片描述

  • 之后按照画背景,画菜单,顺序运行两个函数,这样可以保证棋盘和菜单显示在背景图片之上。

  • 在画好界面后,需要实现相关的功能,这些功能全部都是通过鼠标反馈的,包括鼠标的移动和点击事件。比如,当鼠标在不同游戏状态下移动到棋盘或者移动到菜单位置上,光标的样式都会不同。点击菜单,落子,也会产生不同的反馈。这些功能全部是通过drawpress和drawmove两个函数实现的。

  • 之后判断是否赢棋,以及更新赢棋后的界面。

  • 在完成一帧内的所有操作以及判断后,刷新界面,查看restart的状态以判断用户是否退出程序或者点击重新开始

def mainloop():
	#设置默认光标样式
	pg.mouse.set_cursor(*pg.cursors.arrow)
	gloval.setval('restart',0)
	global restart
	
	restart=gloval.getval('restart')
	gloval.setval('press_intro', 0)
	gloval.setval('press_regret', 0)
	global gamestate 
	gamestate=1
	gloval.setval('gamestate', 1)
	global press,button
	press=(0,0,0)
	button=(132,71,34)#字体颜色
	global screen
	screen=gloval.getval('screen')
	global imBackground
	imBackground=gloval.getval('imBackground')
	global imChessboard
	imChessboard=gloval.getval('imChessboard')
	global imBlackPiece
	imBlackPiece=gloval.getval('imBlackPiece')
	global imWhitePiece
	imWhitePiece=gloval.getval('imWhitePiece')
	global whiteround #回合,黑子先走,whiteround为-1
	whiteround=[-1]
	global chess_array#储存双方落子信息,0246先手,1357后手
	chess_array=[]
	global	chess_num
	chess_num=0
	global piece_x,piece_y,piece
	FPS = 60
	piece_x=[]
	piece_y=[]
	piece=[]
	
	while restart == 0:
		#刷新gamestate
		gamestate=gloval.getval('gamestate')
		#画背景,左上角是坐标	
		drawbg()	
		#画菜单
		drawmenu()
		#鼠标事件按键情况
		drawpress()
		# 画鼠标移动的相关效果
		drawmove()
		#判断是否赢棋
		judgewin.check_win(chess_array)
		#刷新画面
		pg.display.update() 
		#调整游戏帧数
		FPSClock=pg.time.Clock()
		FPSClock.tick(FPS)
		restart=gloval.getval('restart')

画背景 : drawbg()

  • 背景图片从窗口的(0,0)位置,也就是最左上角开始放置,因为签名图像剪切使背景图片大小等于窗口大小,所以铺满整个窗口。
  • 棋盘图片显示同理。
def drawbg():
	##画背景
	screen.blit(imBackground,(0,0)) 
	global chessboard_start_x
	global chessboard_start_y
	chessboard_start_x=50			#(1024-(1024-540))/2
	chessboard_start_y=(768-540)/2		
	screen.blit(imChessboard,(chessboard_start_x,chessboard_start_y))

画菜单 : drawmenu()

对于每一种游戏状态下的菜单都不相同,具体实现方法见menu模块

def drawmenu():
    ##画菜单
	if gamestate==1:				
		menu.menu1()	#画’开始游戏‘,‘游戏说明’,’结束游戏‘按钮
	elif gamestate==2:				
		menu.menu2()	#画 ‘人机对战’,‘双人对战’,'返回上级菜单',‘结束游戏’
	elif gamestate==3:				
		menu.menu3()	#画 ‘玩家先手’,‘电脑先手’,'返回上级菜单',‘结束游戏’
	elif gamestate==4 or gamestate==5 or gamestate==6 :
		menu.menu4()	#画‘悔棋’,‘重新开始’,‘结束游戏’按钮
	elif gamestate==7:				
		menu.menu7()	#画‘重新开始’,‘结束游戏’按钮
		

鼠标移动 :drawmove()

  • 通过pygame的mouse函数获取鼠标的坐标
  • 在4,5,6游戏状态下(都是对战状态),当光标移动到棋盘时,光标才会变成白子或者黑子。
  • 在对应的游戏状态下,光标移动到菜单,会得到相应的反馈,具体实现见menu模块。
	##画鼠标移动的相关效果
def drawmove():

	gloval.setval('mouse_x', pg.mouse.get_pos()[0])	# 鼠标的坐标
	gloval.setval('mouse_y', pg.mouse.get_pos()[1])
	mouse_x,mouse_y = pg.mouse.get_pos()			# 棋子跟随鼠标移动
	if chessboard_start_x<mouse_x<chessboard_start_x+540 and chessboard_start_y<mouse_y<chessboard_start_y+540 and (gamestate==4 or gamestate== 5 or gamestate== 6):
		if whiteround[chess_num]==1:
			screen.blit(imWhitePiece,(mouse_x-16,mouse_y-16))
		else:
			screen.blit(imBlackPiece,(mouse_x-16,mouse_y-16))
	elif gamestate==1:
		menu.movemenu1()
	elif gamestate==2:
		menu.movemenu2()
	elif gamestate==3:
		menu.movemenu3()
	elif gamestate==4  or gamestate==5 or gamestate==6 :
		menu.movemenu4()
	elif gamestate==7:
		menu.movemenu7()
		

鼠标点击:drawpress()

  • 在 for event in pg.event.get()之前为初始化操作,在用到时将会详细说明,下面将详细说明循环内的步骤
  • 首先,通过pygame的event.get获取鼠标点击事件
  • 当把程序X掉时通过pg.quit关闭窗口,通过sys.exit结束程序
  • pygame.mousebuttondown获取鼠标点击事件
  • 在两种情况下点击鼠标会得到反馈,一种是点击菜单,另一种是在对战状态下点击棋盘进行落子。
  • 点击棋盘某个位置进行落子时分为两种情况:当双人对战时,只需要返回鼠标点击的位置,进行后续处理,当人机对战时,因为我默认电脑先手落子于棋盘正中间,之后进行“先人后机”的过程,因此都只需要返回鼠标点击位置,之后在进行一步电脑落子即可。
  • ifem是ifempty的缩写,用于判断落子点是否为空位,如果是空位才能落子,否则不能落子
  • 在点击菜单时,同样需要判断游戏当前状态,执行对应操作,具体实现见menu模块
  • 在落子成功后,需要画棋子和上面的数字,以及最后一个棋子上面的框框。
  • pressed_x,pressed_y是鼠标点击的坐标
def drawpress():

	global whiteround
	global chess_array
	global d
	global chess_num
	global piece_x,piece_y,piece
	press_intro=gloval.getval('press_intro')
	press_regret=gloval.getval('press_regret')
	d = (518-22)/14  			 	#(1,1)的实际坐标为(22,22),(15,15)的实际坐标为(518,518),有14个间隔
	for event in pg.event.get():	#获取鼠标点击事件
		if event.type==pg.QUIT:
			pg.quit()
			exit()
		if event.type == pg.MOUSEBUTTONDOWN:
			gloval.setval('pressed_x', event.pos[0])
			gloval.setval('pressed_y', event.pos[1])
			pressed_x,pressed_y=event.pos[0],event.pos[1]
			#第一种情况,人人对战
			if chessboard_start_x<pressed_x<chessboard_start_x+540 and chessboard_start_y<pressed_y<chessboard_start_y+540 and gamestate==4 :
				player_pos_chess(pressed_x,pressed_y)	
			#第二种情况,玩家先手	
			elif chessboard_start_x<pressed_x<chessboard_start_x+540 and chessboard_start_y<pressed_y<chessboard_start_y+540 and gamestate==5 :
				ifem=player_pos_chess(pressed_x, pressed_y) 	#玩家下棋
				if ifem!=False:
					pc_pos_chess() #电脑下棋
			#第三种情况,电脑先手
			elif chessboard_start_x<pressed_x<chessboard_start_x+540 and chessboard_start_y<pressed_y<chessboard_start_y+540 and gamestate==6:
				ifem=player_pos_chess(pressed_x, pressed_y) 	#玩家下棋
				if ifem!=False:
					pc_pos_chess() #电脑下棋
			#第四种情况,点击菜单
			
			else:									
				if gamestate==1:
					menu.pressmenu1()
				elif gamestate==2:
					menu.pressmenu2()
				elif gamestate==3:
					if 6==menu.pressmenu3():
						pc_pos_chess()
				elif gamestate==4 or gamestate==5 or gamestate==6 :
					menu.pressmenu4()
				elif gamestate==7:
					menu.pressmenu7()
	restart=gloval.getval('restart')#是否重启游戏		
	if chess_array :				#画棋子和棋子上面的数字不为空,有落子
		draw_chess(chess_num,whiteround)	#画棋子和上面的数字
	if press_intro==1: 						#游戏简介信息
		draw_intro_text()	
	if press_regret==1:				#悔棋
		regret()
	draw_chesscross(chess_num)#画最后一个落子位置

游戏简介显示 :draw_intro_text()

  • 当在主菜单页面点击“游戏说明”时显示相关内容。我在使用系统默认字体的时候总是报错,所以使用了网上下载的字体文件。

  • render的pygame官方定义文档如下,button是我定义的颜色,button=(132,71,34)
    在这里插入图片描述

  • blit函数是把图片放到对象上screen.blit(image,position)表示将image放置到screen对象上,位置的两个坐标由position确定,pygame官方文档对blit的解释如下
    在这里插入图片描述

def draw_intro_text():
	my_font=pg.font.Font('mufont.ttf',25)
	text1=my_font.render("双方分别使用黑白两色的棋子,",True,button)
	text2=my_font.render("下在棋盘直线与横线的交叉点上,",True,button)
	text3=my_font.render("先形成五子连线者获胜。",True,button)
	screen.blit(text1,(640,100))	
	screen.blit(text2,(640,140))	
	screen.blit(text3,(640,180))	

悔棋 : regret ()

  • 实现方法:黑白双方每一次落子,chess_num加一,如果chess_num不为0则代表有落子,可以进行悔棋。
  • 当双人对战时,只需要悔一步棋,也就是把最后一步落子的相关信息,落子位置全部删除,同时落子方更换,比如现在是白方回合选择了悔棋,在悔棋后将是黑方回合
  • 当人机对战时,需要一次性悔两步,一步是玩家,一步是电脑,因为电脑不存在悔棋,连续悔棋两步才会重新轮到玩家回合。
  • 至于chess_array,piece_x,piece_y,piece,whiteround的具体含义,将会在后面详细介绍。
def regret():
	global chess_num,chess_array,piece_y,piece_x,piece,whiteround
	if chess_num!=0:  #删除所有储存的数组
		if gamestate==4:
			del chess_array[-1]
			del piece_x[-1]
			del piece_y[-1]
			del piece[-1]
			del whiteround[-1]
			chess_num=chess_num-1
		if gamestate==5 or gamestate==6:
			del chess_array[-1]
			del chess_array[-1]
			del piece_x[-1]
			del piece_y[-1]
			del piece_x[-1]
			del piece_y[-1]
			del piece[-1]
			del piece[-1]
			del whiteround[-1]
			del whiteround[-1]
			chess_num=chess_num-2
	gloval.setval('press_regret', 0)

画棋子上的数字 :draw_chess()

  • 实现的效果是在每个棋子上面标注数字,黑子用白字标,白子用黑字标,方便玩家看清落子顺序,分析棋局。效果如下:

  • chess_num储存了棋面上的棋子总数。

  • 不管是人机对战还是双人对战,第一个落子的总是黑子。

  • whileround间隔储存1和-1,1代表白子回合,-1代表黑子回合。

  • 以黑子落子为例,screen.blit(imBlackPiece,(piece_x[i]-16,piece_y[i]-16))将黑子的中心恰好放到网格线的交叉点上。

  • i从0开始计数,所以第i 个子上的数字为i+1。
    将数字恰好放到棋子上,而且先显示棋子,后显示数字,这样数字就不会被棋子覆盖。

def draw_chess(chess_num,whiteround):
	my_font=pg.font.Font('mufont.ttf',18)
	for i in range(chess_num):
		
		if whiteround[i]==-1:				#黑子
			screen.blit(imBlackPiece,(piece_x[i]-16,piece_y[i]-16))
			text_w=my_font.render(str(i+1),True,(255,255,255))
			screen.blit(text_w,(piece_x[i]-7,piece_y[i]-12))
		else:								#白子
			screen.blit(imWhitePiece,(piece_x[i]-16,piece_y[i]-16))	
			text_b=my_font.render(str(i+1),True,(0,0,0))
			screen.blit(text_b,(piece_x[i]-7,piece_y[i]-12))

画最后一个棋子的落子位置:draw_chesscross()

  • 如果有落子,那么就画一个方框吧最后一个棋子围住就好,button是自定义的颜色元祖,pygame中draw.rect的解释文档如下:
    在这里插入图片描述
  • 第三个参数Rect=[A,B,C,D],其中ABCD必须是整数,AB存储起始位置,CD存储矩形长宽,width为矩形边框的粗细。
  • piece_x和piece_y两个列表按照落子顺序存储了棋子的横纵坐标。

def draw_chesscross(chess_num):
	if chess_num!=0:
		pg.draw.rect(screen,button,[int(piece_x[chess_num-1]-16),int(piece_y[chess_num-1]-16),33,33],2)
	
		
	##二维坐标转一维索引值

一维索引和二维坐标间的转换:array2index(),index2array()

	##二维坐标值转一维索引
def array2index(array):
	return (array[0]-1)*15+array[1]

	
	##一维索引值转二维坐标
def index2array(index):
	i,j = int((index-index%15)/15+1),int(index%15)
	if j == 0 :
		i -= 1
		j = 15
	return i,j

判断是否为空:if_isempty

输入变量是落子的横纵坐标,和piece中存储的棋面上已落子的坐标列表对比

	##判断是否为空
def if_isempty(i,j):
	if [i,j] not in  piece:
		return True
	else:
		return False
坐标位置:getpos(),getrealpos()
  • getpos()的作用是吧mousebuttondown返回的坐标pressed_x,pressed_y转变为棋盘上的位置,比如(1,3),(1,15)如下图所示。

在这里插入图片描述

  • 因为玩家落子不会正好点击到网格点上,所以getpos()在实现转化的时候需要进行近似处理。也就是说点在点在每个红框内,就会round()到相应的棋盘位置。
    -在这里插入图片描述
  • getrealpos()的作用是把棋盘上的位置转化为真实的位置,可以理解成getpos的逆操作,return的值就是screen.blit()落子输入的位置参数。
	##得到棋盘上棋子的数组位置
def getpos(pressed_x,pressed_y):
	mouse_chessboard_x = pressed_x-chessboard_start_x# 鼠标在棋盘中的坐标
	mouse_chessboard_y = pressed_y-chessboard_start_y
	i_tmp = round((mouse_chessboard_y-22)/d)+1	# 计算鼠标最接近的格点
	j_tmp = round((mouse_chessboard_x-22)/d)+1
	if i_tmp in range(1,16) and j_tmp in range(1,16):#1到15判断标号是否有效
		chess_i = i_tmp
		chess_j = j_tmp
		return chess_i,chess_j
		
	##二维坐标转化为棋盘内的真实坐标(计算棋子在棋盘中的位置)(计算棋子的实际位置)
def getrealpos(array):    											
	piece_chessboard_x,piece_chessboard_y = 22+(array[1]-1)*d,22+(array[0]-1)*d
	piece_x,piece_y = piece_chessboard_x+chessboard_start_x,piece_chessboard_y+chessboard_start_y
	return piece_x,piece_y

玩家落子:player_pos_chess()

  • 传入参数是鼠标点击的坐标位置
  • 执行的操作有:将转化好的相对于棋盘的落子位置存入chess_array,判断落子位置是否为空如果为空则存入相关信息,如果不为空则删除重复的数组
def player_pos_chess(pressed_x,pressed_y):
	global whiteround
	global chess_array
	global chess_num
	global piece_x,piece_y,piece
	chess_array.append(list(getpos(pressed_x,pressed_y)))#记录位置,黑棋白棋数组交替
	piece_i,piece_j=getrealpos(chess_array[chess_num])#已处理的位置
	isempty=if_isempty(piece_i,piece_j)
	
	if isempty==True:		#如果这个地方没有棋子		
		whiteround.append(-whiteround[chess_num])
		piece_x.append(piece_i)
		piece_y.append(piece_j)
		piece.append([piece_i,piece_j])
		chess_num+=1
		gloval.setval("chess_array", chess_array)
		return 1
	else:								#	如果这个地方有棋子
		del chess_array[chess_num]		#删除那个重复的数组		
		return 0

电脑落子:pc_pos_chess()

电脑落子的处理步骤和玩家落子相比唯一的不同是不需要处理鼠标点击信息,因为电脑通过算法得到的落子位置就是相对于棋盘的位置,由pc_pressed()得到。

def pc_pos_chess():
	global whiteround
	global chess_array
	global chess_num
	global piece_x,piece_y,piece
	pc_pressed=easy_pc.find_maxscore(chess_array,whiteround[-1])
	chess_array.append(pc_pressed)#记录电脑下棋位置
	piece_i,piece_j=getrealpos(chess_array[chess_num])#已处理的位置
	isempty=if_isempty(piece_i,piece_j)
	print(isempty)
	if isempty==True:									#如果这个地方没有棋子
		whiteround.append(-whiteround[chess_num])
		piece_x.append(piece_i)
		piece_y.append(piece_j)
		piece.append([piece_i,piece_j])
		chess_num+=1
		gloval.setval("chess_array", chess_array)
	else:								#	如果这个地方有棋子
		del chess_array[chess_num]		#删除那个重复的数组	

windows. py (预处理模块)

导入模块

  • PIL模块用于修改素材尺寸,gloval模块用于管理文件间全局变量。
#--coding:utf-8--
from PIL import Image 
import pygame as pg
from sys import * #用exit来退出程序
import gloval 
gloval.init()

素材处理 : Image_PreProcessing()

  • 因为棋盘,棋子,背景颜色的素材均来源于网络,大小不一定符合要求,所有图片在使用之前需要先进行长宽裁剪操作

def Image_PreProcessing():
	# 图片存储路径	
	im = Image.open('background.jpeg')
	# Resize图片大小,入口参数为一个tuple,为新的图片大小
	imBackground = im.resize((424,300))
	imBackground.save('1111.jpg','JPEG')

	
	im = Image.open('chessboard.jpeg')
	# Resize图片大小,入口参数为一个tuple,为新的图片大小
	imBackground = im.resize((540,540))
	imBackground.save('new_chessboard.jpg','JPEG')
	
	
	im = Image.open('chessblack.png')
	# Resize图片大小,入口参数为一个tuple,为新的图片大小
	imBackground = im.resize((32,32))
	imBackground.save('new_chessblack.png','PNG')

	im = Image.open('chesswhite.png')
	# Resize图片大小,入口参数为一个tuple,为新的图片大小
	imBackground = im.resize((32,32))
	imBackground.save('new_chesswhite.png','PNG')	
	

加载素材以及pygame的初始化 : windows()

  • 在pygame中可以使用pygame.image.load()函数来加载位图。(支持jpg,png,gif,bmp,pcx,tif,tga等多种图片格式),具体方法是image = pygame.image.load(“image.png”).convert_alpha()
  • convert_alpha()方法会使用透明的方法绘制前景对象,这里我使用的黑子和白子有alpha通道,如果不使用convert_alpha(),棋盘上显示的将会是如下效果(下图中白子素材使用了convert_alpha方法导入,但是黑子素材只是用了convert方法)

在这里插入图片描述

  • gloval.setval(A, B)的意思是A=B,其中A和B可以是跨文件的全局变量,具体实现方法见gloval .py。
  • gloval.setval(‘screen’, pg.display.set_mode((1024,768),0,32))设置窗口的大小为1024*768
def windows():
	pg.init()  #初始化pygame,为使用硬件做准备 
#	screen=pg.display.set_mode((1024,768),0,32)#分辨率,标志位,色深
	#加载背景和光标图片
	gloval.setval('screen', pg.display.set_mode((1024,768),0,32))
	gloval.setval('imBackground', pg.image.load('new_background.jpg').convert())
	gloval.setval('imChessboard', pg.image.load('new_chessboard.jpg').convert())
	gloval.setval('imBlackPiece', pg.image.load('new_chessblack.png').convert_alpha())
	gloval.setval('imWhitePiece', pg.image.load('new_chesswhite.png').convert_alpha())

	pg.display.set_caption('五子棋      by Ace Cheney') #设置窗口标题

global. py (全局变量管理模块)

  • 用字典来保存工程全局变量,getval函数实现全局变量的存入,setval实现全局变量的取出
def init():
	global glodic
	glodic = {}
def setval(name,val):
	glodic[name]=val
def getval(name,defva=None):
	return glodic[name]

menu. py(菜单模块)

颜色定义:press=(0,0,0),button=(132,71,34)
这一块主要是实现各个游戏状态下的菜单界面以及功能,因为4,5,6号游戏状态菜单相同,所以通过menu4来实现。

在这里插入图片描述
下面通过举例menu1的三个函数,来看看具体的实现方法。

通常情况下的菜单显示: menu1()

  • pg.draw.rect()方法画三个菜单按钮
  • screen.blit()显示三个文本框
def menu1():
	screen=gloval.getval('screen')

	#画’开始游戏‘,‘游戏说明’,’结束游戏‘按钮
	pg.draw.rect(screen,button,[670,260,200,80],5)
	pg.draw.rect(screen,button,[670,410,200,80],5)
	pg.draw.rect(screen,button,[670,560,200,80],5)

	my_font=pg.font.Font('mufont.ttf',45)
	text1=my_font.render("开始游戏",True,button)
	text2=my_font.render("游戏说明",True,button)
	text3=my_font.render("结束游戏",True,button)
	screen.blit(text1,(680,270))
	screen.blit(text2,(680,420))
	screen.blit(text3,(680,570))

当鼠标移动到菜单上: movemenu1()

  • 如果鼠标移动到相应菜单显示的区域,鼠标的光标样式改变pg.mouse.set_cursor(),参数*pg.cursors.broken_x是准星
  • 菜单颜色发生变化,颜色从button(132,71,34)变成press(0,0,0)
def movemenu1():
	mouse_x=gloval.getval('mouse_x')
	mouse_y=gloval.getval('mouse_y')
	screen=gloval.getval('screen')
	my_font=pg.font.Font('mufont.ttf',45)

	if 670<mouse_x<870 and 260<mouse_y<340:
		pg.mouse.set_cursor(*pg.cursors.broken_x)
		pg.draw.rect(screen,press,[670,260,200,80],5)
		text1=my_font.render("开始游戏",True,press)

		screen.blit(text1,(680,270))
	elif 670<mouse_x<870 and 410<mouse_y<490:
		pg.mouse.set_cursor(*pg.cursors.broken_x)

		pg.draw.rect(screen,press,[670,410,200,80],5)
		text2=my_font.render("游戏说明",True,press)
		screen.blit(text2,(680,420))
	elif 670<mouse_x<870 and 560<mouse_y<640:
		pg.mouse.set_cursor(*pg.cursors.broken_x)

		pg.draw.rect(screen,press,[670,560,200,80],5)
		text3=my_font.render("结束游戏",True,press)
		screen.blit(text3,(680,570))	
	else:
		pg.mouse.set_cursor(*pg.cursors.arrow)

当鼠标点击时:pressmenu1()

def pressmenu1():
	screen=gloval.getval('screen')
	my_font=pg.font.Font('mufont.ttf',45)
	pressed_x=gloval.getval('pressed_x')
	pressed_y=gloval.getval('pressed_y')
	if 670<pressed_x<870 and 260<pressed_y<340:#开始游戏,进入菜单2
		gloval.setval('gamestate', 2)
		gloval.setval('press_intro', 0)

	elif 670<pressed_x<870 and 410<pressed_y<490:#游戏介绍
		gloval.setval('press_intro', 1)
	elif 670<pressed_x<870 and 560<pressed_y<640:#退出游戏
		pg.quit()
		exit()
	else :
		gloval.setval('press_intro', 0)	

其他部分原理相同,不再赘述,有特殊功能的特殊解决即可,比如游戏状态二的“返回上级菜单”功能,只需要gloval.setval(‘gamestate’, 1)即可,比如悔棋,只需要gloval.setval(‘press_regret’, 1)即可。

easy_pc.py 人机对战模块

导入库以及打分表的设计

值得注意的是:

  • 打分表是给电脑看的,是计算机寻找落子位置的依据。
  • 这里的打分表不能针对白子或者黑子设计,而应该针对己方和对方设计,因为不管自己先手或者后手,电脑的评判标准都应该是一样的。有很多人设计五子棋程序发现人机对战时自己先手比后手简单,或者自己先手比后手难,很大可能是打分表的设计有误。
  • 这里的分数是一个五连位置将一个五连位置看成整体,对这个五连位置打的分数,五连包括横五连,竖五连,以及斜五连。
#--coding:utf-8--
from PIL import Image 
import pygame as pg
from sys import exit #用exit来退出程序
import gloval 
import random
gloval.init()
##打分表
tuple_score=[None]*10
tuple_score[0]=7                #没有子
tuple_score[1]=35				#一个己方子
tuple_score[2]=800				#两个己方子
tuple_score[3]=15000			#三个己方子
tuple_score[4]=800000			#四个己方子
tuple_score[5]=15				#一个对方子
tuple_score[6]=400				#两个对方子
tuple_score[7]=8000			#三个对方子
tuple_score[8]=100000			#四个对方子
tuple_score[9]=0				#又有白又有黑

计算一个空位的分数chess_score()

  • 只需要寻找空位计算分数即可,因为分数是为电脑落子提供评判依据提供的服务的,已有落子的位置计算机不能再次落子,自然不用计算分数
  • 一个位置的分数是所有包括该位置的五连的分数的总和,以横五连为例,包括以下五种情况,如下图所示:,除了横五连,还有竖五连,左斜,右斜,一共20种情况
    在这里插入图片描述
	##计算一个空位的分数(需要棋谱数组和空位位置,返回该位置的分数)
def chess_score(array,chess_pos,whiteround):
	pos_score=0	
	x=chess_pos[0]
	y=chess_pos[1]
	black_num=0
	white_num=0	
	##竖列
	for i in range(5):#1234  统计竖列所有五元组的得分总和
		for j in range(5):#01234 统计一个五元组的得分
			if [x-j+i,y] in array[::2]:#黑子判断 #横向
				black_num+=1
			if [x-j+i,y] in array[1::2]:#白子判断 #横向
				white_num+=1
		pos_score=pos_score+chess_tuple_score(black_num, white_num,whiteround)#计算一个元组
		white_num=0
		black_num=0
	##横列
	for i in range(5):
		for j in range(5):
			if [x,y-j+i] in array[::2]:#黑子判断 #横向
				black_num+=1
			if [x,y-j+i] in array[1::2]:#白子判断 #横向
				white_num+=1
		pos_score=pos_score+chess_tuple_score(black_num, white_num,whiteround)
		white_num=0
		black_num=0
	##左斜/
	for i in range(5):
		for j in range(5):
			if [x+j-i,y-j+i] in array[::2]:#黑子判断 #横向
				black_num+=1
			if [x+j-i,y-j+i] in array[1::2]:#白子判断 #横向
				white_num+=1
		pos_score=pos_score+chess_tuple_score(black_num, white_num, whiteround)
		white_num=0
		black_num=0
	##右斜\	
	for i in range(5):
		for j in range(5):
			if [x-j+i,y-j+i] in array[::2]:#黑子判断 #横向
				black_num+=1
			if [x-j+i,y-j+i] in array[1::2]:#白子判断 #横向
				white_num+=1
		pos_score=pos_score+chess_tuple_score(black_num, white_num, whiteround)
		white_num=0
		black_num=0
	return pos_score

计算一个五连的分数:chess_tuple

字面意思,计算一个五连的分数,别忘了在计算之前先判断自己是白子还是黑子,判断方法很简单,在白方回合的白子就是己方子,黑子就是敌方子,反之亦然。

def chess_tuple_score(black_num,white_num,whiteround):
	if black_num==0 and white_num==0: 	#没有子
		pos_tuple_score=tuple_score[0]
	elif black_num>0 and white_num>0: 	#又有白又有黑
		pos_tuple_score=tuple_score[9]
	else:                             	#只有黑或者只有白
		if whiteround == -1:
			if black_num != 0:				#计算横着一格黑子
				pos_tuple_score=tuple_score[black_num]
			if white_num != 0:				#计算横着一格白子
				pos_tuple_score=tuple_score[white_num+4]
		if whiteround == 1:
			if black_num != 0:				#计算横着一格黑子
				pos_tuple_score=tuple_score[black_num+4]
			if white_num != 0:				#计算横着一格白子
				pos_tuple_score=tuple_score[white_num]		
	return pos_tuple_score

找到整个棋盘的最大分数位置find_maxscore()

  • 输入参数是回合和整个棋盘的已有落子的全部位置,这很好理解也很符合常理。返回位置是电脑选择的落子点,值得一提的是,这个点并不一定是得分最高的点;)
  • 对棋盘上所有可以落子位置的得分进行排序,如果得分前三高的的点分数相差不超过50分,则可以近似认为这三个位置棋效相同,随机选一个位置落子,如果不满足上一条件,而前两高分数的落子分差不超过100,则在这两个位置种随机选一个落子。
  • 之所以这样做,是为了避免走出同样的棋局,也就避免了玩家使用“套路”来过于轻松的取胜电脑。
def find_maxscore(array,whiteround):
	if array==[]:
		best_pos=[8,8]
	else:
		chess_score_array=[]
		for row in range(1,15):
			for col in range(1,15):
				chess_pos = [row,col]
				if chess_pos not in array:
					pos_score=chess_score(array,chess_pos,whiteround)
					chess_score_array.append([pos_score, row, col])
		chess_score_array.sort(reverse=True)
		
		#随机落子
		if chess_score_array[0][0]-chess_score_array[2][0]<50:	
			choose_pos=random.randint(0,2)
			
		elif chess_score_array[0][0]-chess_score_array[1][0]<100:	
			choose_pos=random.randint(0,1)
		else :	
			choose_pos=0
		pc_pressed_x=chess_score_array[choose_pos][1]
		pc_pressed_y=chess_score_array[choose_pos][2]
		best_pos=[pc_pressed_x,pc_pressed_y]
		#print(best_pos)
		print(chess_score_array)
	return best_pos

judgewin. py 胜负判断模块

模块导入

#--coding:utf-8--
from PIL import Image 
import pygame as pg
#from pygame.locals import *#导入一些常用的函数和常量
from sys import exit #用exit来退出程序
import easygui 
import gloval 
gloval.init()

显示获胜信息 check_win()

  • 传入参数为棋面落子坐标列表,偶数为黑子,奇数为白子,所以不用单独储存
  • 判断五连为真即为一方赢,落子坐标列表长度等于棋面空位总数225代表和棋
  • 一方胜利或者和棋则代表游戏结束,显示相关信息并且游戏状态跳转到7状态
def check_win(chess_array):#黑色白色棋子的索引值列表
	press=(0,0,0)
	black_array=chess_array[::2]#偶数
	white_array=chess_array[1::2]#奇数
	screen=gloval.getval('screen')
	if five_pieces(black_array)==1:
		# 黑方胜利
		my_font=pg.font.Font('mufont.ttf',55)
		text2=my_font.render("黑方胜利",True,press)
		screen.blit(text2,(680,100))	
		game_end()
	if five_pieces(white_array)==1:
		# 白方胜利
		my_font=pg.font.Font('mufont.ttf',55)
		text2=my_font.render("白方胜利",True,press)
		screen.blit(text2,(680,100))	
		game_end()
	if len(chess_array)== 225:
		# 平局
		my_font=pg.font.Font('mufont.ttf',55)
		text2=my_font.render("和棋",True,press)
		screen.blit(text2,(680,100))	
		game_end()#状态置七

判断是否五连 five_pieces()

  • 判断技巧:不需要遍历整个棋盘判断是否在某个位置出现五连,只需要判断最后一个落子位置有没有实现五连即可。这里传入的参数是白子落子位置列表或者黑子落子位置列表。
def five_pieces(array):
	# 提取最后一颗棋子,负一
	if array:
		x=array[-1][0]
		y=array[-1][1]
		for j in range(1,12):
			#竖五连
			if [x,j] in array and [x,j+1] in array and [x,j+2] in array and [x,j+3] in array and [x,j+4] in array :
				return 1
			#横五连
			if [j,y] in array and [j+1,y] in array and [j+2,y] in array and [j+3,y] in array and [j+4,y] in array :
				
				return 1
		for j in range(5):
			#\五连

			if [x-j,y-j] in array and [x-j+1,y-j+1] in array and [x-j+2,y-j+2] in array and [x-j+3,y-j+3] in array and [x-j+4,y-j+4] in array :
				return 1
			#/五连
			if [x-j,y+j] in array and [x-j+1,y+j-1] in array and [x-j+2,y+j-2] in array and [x-j+3,y+j-3] in array and [x-j+4,y+j-4] in array :

				return 1
		return 0
	

源码及材料已上传CSDN,全剧终,共计两万三千余字。

发布了14 篇原创文章 · 获赞 70 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Accelerato/article/details/92007881