文章目录
- 利用pygame实现一个支持双人对战以及人机对战的小游戏
-
- 最终效果展示
- 总体框架介绍
- 具体功能以及算法思想
- 代码详解
-
- wuziqi. py (核心模块)
- windows. py (预处理模块)
- global. py (全局变量管理模块)
- menu. py(菜单模块)
- easy_pc.py 人机对战模块
- judgewin. py 胜负判断模块
利用pygame实现一个支持双人对战以及人机对战的小游戏
最终效果展示
总体框架介绍
windows.py负责处理素材图片以及将图片导入pygame。menu.py负责各级菜单的具体功能实现,包括菜单的绘制,点击效果,鼠标移动效果,点击返回等等。easypc.py是实现简单人机对战的主要程序,我采用了最简单的打分表,没有录入棋谱,也没有用博弈论决策树alphabeta剪枝,但是同样达到了较好的效果。judgewin.py主要实现对终局棋盘胜负判断。因为python无法直接实现跨文件的全局变量传输,(global 关键字可以定义一个变量为全局变量,但是这个仅限于在一个模块(py文件)中调用全局变量,在另外一个py文件 再次使用 global x 也是无法访问到的)所以我专门定义了一个全局变量管理模块gloval.py来管理全局变量。
具体功能以及算法思想
一、主界面与棋盘设计
- 采用了图形化的界面,一共有四张图片组成,分别是背景图片,棋盘图片,黑子图片,白字图片,菜单为绘图函数绘制。
- 在上部可以看到程序和作者信息:五子棋 by Ace Cheney。
- 当鼠标碰到菜单时,鼠标图标会由指针变成准星,同时方框以及字体变黑。
- 当点击游戏说明时,会出现游戏的相关介绍。
- 当点击开始游戏时,会进入二级菜单。
- 当点击双人人对战时,会进入三级菜单。此时鼠标移动到棋盘,棋子会显示出来。
- 当点击人机对战时,会进入四级菜单,让玩家选择是否先手。
- 当点击悔棋,产生悔棋效果。
- 当点击返回上级菜单时,返回上级菜单。
- 当落子后,会在每个落子上显示棋子的序号,且在最后一个棋子上面显示方框。
二、移位与胜负判定
- 对于移位操作,首先要判断棋子是佛在棋盘内,如果在棋盘内,则显示棋子,如果不在棋盘内,则不显示棋子。
- 对于在棋盘内的情况,需要先找到每个横纵交叉点的位置,即相对于棋盘和游戏界面的横纵坐标。
- 之后利用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