课 程 名:python课程设计
课程设计项目名称:基于python的俄罗斯方块
团队成员: 叶焱镔、柯博群、钱昱铭
一、项目简介
1.1 项目博客地址
1.2 项目完成的功能与特色
俄罗斯方块的游戏实现,实现了随机方块的生成、下落、旋转,游戏的进行、消除、结束,游戏的重新开始、退出、暂停,可显示最高记录、历史记录、记录的排行,可继续上回的游戏
1.3 项目采用的技术栈 python
1.4 项目借鉴源代码的地址https://blog.csdn.net/lanseguhui/article/details/80338332
1.5 团队成员任务分配表
叶焱镔:上回游戏的功能实现
柯博群:记录游戏的得分与排行情况,和退出的提示
钱昱铭:基础功能的设计与实现
二、项目的需求分析
1、实现各种方块的生产,包括形状和颜色等信息;
2、实现各个方块的上下左右移动和旋转的功能
3、实现消行的功能;
4、实现得分的统计功能;
5、实现开始,暂停,结束等功能
6、实现历史记录最高记录与排行
7、实现上回游戏继续
三、项目功能架构图、主要功能流程图
四、系统模块说明
4.1 系统模块列表
4.2 各模块详细描述(名称,功能,运行截图,关键源代码)
class RussiaBlock(object):
#初始化地图
def __init__(self):
self.newflag=True
self.sflag=False
#判断是否开始(记录游戏记录合法标志
self.create_not_exist()
# 方块颜色列表
self.color = ['red', 'orange', 'yellow', 'purple', 'blue', 'green', 'pink']
# 字典 存储形状对应7种形状 元组存储坐标
self.shapeDict = {1: [(0, 0), (0, -1), (0, -2), (0, 1)], # shape I
2: [(0, 0), (0, -1), (1, -1), (1, 0)], # shape O
3: [(0, 0), (-1, 0), (0, -1), (1, 0)], # shape T T型
4: [(0, 0), (0, -1), (1, 0), (2, 0)], # shape J 右长倒L盖子
5: [(0, 0), (0, -1), (-1, 0), (-2, 0)], # shape L
6: [(0, 0), (0, -1), (-1, -1), (1, 0)], # shape Z
7: [(0, 0), (-1, 0), (0, -1), (1, -1)]} # shape S
# 旋转坐标控制
self.rotateDict = {(0, 0): (0, 0), (0, 1): (-1, 0), (0, 2): (-2, 0), (0, -1): (1, 0),
(0, -2): (2, 0), (1, 0): (0, 1), (2, 0): (0, 2), (-1, 0): (0, -1),
(-2, 0): (0, -2), (1, 1): (-1, 1), (-1, 1): (-1, -1),
(-1, -1): (1, -1), (1, -1): (1, 1)}
# 初始高度,宽度 核心块位置
self.coreLocation = [4, -2]
self.height, self.width = 20, 10
self.size = 32
# map_s can record the location of every square.i宽j高
self.map_s = self.rmyRecord()
if not bool(self.map_s):
# 全部置0
for i in range(self.width):
for j in range(-4, self.height):
self.map_s[(i, j)] = 0
# 添加边界
for i in range(-4, self.width + 4):
self.map_s[(i, self.height)] = 1
for j in range(-4, self.height + 4):
for i in range(-4, 0):
self.map_s[(i, j)] = 1
for j in range(-4, self.height + 4):
for i in range(self.width, self.width + 4):
self.map_s[(i, j)] = 1
# 初始化分数0默认不加快 按下时加快
self.score = 0
self.isFaster = False
# 创建GUI界面
self.root = Tk()
self.root.title("RussiaBlock")
self.root.geometry("500x645")
self.area = Canvas(self.root, width=320, height=640, bg='white')
self.area.grid(row=2)
self.pauseBut = Button(self.root, text="暂停", height=2, width=13, font=18, command=self.is_pause)
self.pauseBut.place(x=340, y=100)
self.startBut = Button(self.root, text="开始", height=2, width=13, font=18, command=self.play)
self.startBut.place(x=340, y=20)
self.restartBut = Button(self.root, text="重新开始", height=2, width=13, font=18, command=self.is_restart)
self.restartBut.place(x=340, y=180)
self.quitBut = Button(self.root, text="退出", height=2, width=13, font=18, command=self.is_quit)
self.quitBut.place(x=340, y=260)
self.scoreLabel1 = Label(self.root, text="分数:", font=24)
self.scoreLabel1.place(x=340, y=400)
self.scoreLabel2 = Label(self.root, text="0", fg='red', font=24)
self.scoreLabel2.place(x=410, y=400)
#最高记录------------------------
self.scoreLabel3 = Label(self.root, text="最高记录:", font=24)
self.scoreLabel3.place(x=340, y=480)
maxs=self.getMax()[0]
self.scoreLabel4 = Label(self.root, text=maxs, fg='red', font=24)
self.scoreLabel4.place(x=450, y=480)
#排行/
self.sortBut = Button(self.root, text="排行", height=1, width=8, font=12, command=self.getSort)
self.sortBut.place(x=340, y=510)
#历史记录------------------------
self.recordBut = Button(self.root, text="历史记录", height=1, width=8, font=12, command=self.getRecord)
self.recordBut.place(x=340, y=550)
# 按键交互
self.area.bind("", self.rotate)
self.area.bind("", self.move_left)
self.area.bind("", self.move_right)
self.area.bind("", self.move_faster)
self.area.bind("", self.rotate)
self.area.bind("", self.move_left)
self.area.bind("", self.move_right)
self.area.bind("", self.move_faster)
self.area.focus_set()
# 菜单
self.menu = Menu(self.root)
self.root.config(menu=self.menu)
self.startMenu = Menu(self.menu)
self.menu.add_cascade(label='开始', menu=self.startMenu)
self.startMenu.add_command(label='新游戏', command=self.is_restart)
self.startMenu.add_separator()
self.startMenu.add_command(label='重新开始', command=self.play)
self.exitMenu = Menu(self.menu)
self.menu.add_cascade(label='退出', command=self.is_quit)
self.helpMenu = Menu(self.root)
self.menu.add_cascade(label='帮助', menu=self.helpMenu)
self.helpMenu.add_command(label='如何操作', command=self.rule)
self.helpMenu.add_separator()
self.helpMenu.add_command(label='关于...', command=self.about)
# 先将核心块的所在位置在map_s中的元素设为1,通过self.shapeDict获取其余方块位置,将map_s中对应元素设为1。
def get_location(self):
map_s[(core[0], core[1])] = 1
for i in range(4):
map_s[((core[0] + getNew[i][0]), (core[1] + getNew[i][1]))] = 1
# 判断方块下移一格后对应位置map_s中的元素是否为一,是,则不可移动,返回False;否,可以移动,返回True。
def can_move(self):
for i in range(4):
if map_s[(core[0] + getNew[i][0]), (core[1] + 1 + getNew[i][1])] == 1:
return False
return True
# 先用randRange获取1~7中的随机整数,随机到某一整数,那么访问self.shapeDict,获取这种形状方块的核心块及其他方块的相对位置。
# 访问颜色字典,获取此方块的颜色。建立循环,当方块可移动时(while self. can_move():),且暂停键未被摁下(if is_pause:),
# 核心块纵坐标加一,根据核心块及其他方块对于核心块的相对位置,画出四个方块。用self.get_location()函数获取方块的位置。
def draw_new(self):
global next_s
global getNew
global core
core = [4, -2]
next_s=randrange(1, 8)
if self.newflag:
core=self.rmysq()[0:2]
next_s=self.rmysq()[2]
self.scoreLabel2.config(text=str(self.rmysq()[-1]))
# 形状
self.newflag=False
getNew = self.shapeDict[next_s]
time = 0.2
while self.can_move():
if is_pause:
core[1] += 1
self.draw_square()
if self.isFaster:
sleep(time - 0.15)
else:
sleep(time + 0.22)
self.isFaster = False
else:
self.draw_square()
sleep(time)
self.get_location()
# 绘制当前方块
def draw_square(self):
self.area.delete("new")
for i in range(4):
self.area.create_rectangle((core[0] + getNew[i][0]) * self.size,
(core[1] + getNew[i][1]) * self.size,
(core[0] + getNew[i][0] + 1) * self.size,
(core[1] + getNew[i][1] + 1) * self.size,
fill=self.color[next_s - 1], tags="new")
self.area.update()
# 给底部每行中方块都加上标签:bottom + str(j), j代表该块所在行数,每次遍历map_s,建立对于range(self. height)的for循环,删去每一行,
# 若map_s什么地方的元素为1,画出这一位置的方块,不断更新。这样可以画出底部方块。
def draw_bottom(self):
for j in range(self.height):
self.area.delete('bottom' + str(j))
for i in range(self.width):
if map_s[(i, j)] == 1:
self.area.create_rectangle(self.size * i, self.size * j, self.size * (i + 1),
self.size * (j + 1), fill='grey', tags='bottom' + str(j))
self.area.update()
# 判断填满遍历map_s每一行的各个元素,若所有元素为1,则标签中score值+10,将
# 此行所有元素改为0,行数map_s(i,j)=map_s(i-1,j)(即所有之上的行下移)
# ,那么后续画底部方块时,可实现消行。
def is_fill(self):
for j in range(self.height):
t = 0
for i in range(self.width):
if map_s[(i, j)] == 1:
t = t + 1
if t == self.width:
self.get_score()
self.delete_line(j)
# 加分 每一行+10
def get_score(self):
score_value = eval(self.scoreLabel2['text'])
score_value += 10
self.scoreLabel2.config(text=str(score_value))
# 消行
def delete_line(self, j):
for t in range(j, 2, -1):
for i in range(self.width):
map_s[(i, t)] = map_s[(i, t - 1)]
for i in range(self.width):
map_s[(i, 0)] = 0
self.draw_bottom()
# 遍历每一行,若从顶部到底部map_s每一行都有某一个元素或更多元素为1,
# 那么说明方块以顶到最上端,游戏结束。此处不可以简单判定最上一行是否有元素为1就判定结束,
# 若这样会产生顶部有新的方块产生,然后导致顶部有元素为1,误判为游戏结束。
def is_over(self):
t = 0
for j in range(self.height):
for i in range(self.width):
if self.map_s[(i, j)] == 1:
t += 1
break
if t >= self.height:
return False
else:
return True
# 先判断方块是否可以旋转(针对其靠近边界时)。先将其现在所在位置对应map_s中的元素改为0,判断其旋
# 转后位置对应map_s中的元素是否有一,若有,说明其旋转后的位置已经被占,是不能旋转的,返回值为False
# 。否则为可旋转,返回值True。若已判定可以旋转,那么访问self.rotateDict,得出旋转以后所有小块的位置
# 变换,将变换以后的位置对应map_s的元素设为1,旋转便已完成。
def can_rotate(self):
for i in range(4):
map_s[((core[0] + getNew[i][0]),
(core[1] + getNew[i][1]))] = 0
for i in range(4):
if map_s[((core[0] + self.rotateDict[getNew[i]][0]),
(core[1] + self.rotateDict[getNew[i]][1]))] == 1:
return False
return True
# 旋转
def rotate(self, event):
if next != 2:
if self.can_rotate():
for i in range(4):
getNew[i] = self.rotateDict[getNew[i]]
self.draw_square()
if not self.can_move():
for i in range(4):
map_s[((core[0] + getNew[i][0]), (core[1] + getNew[i][1]))] = 1
# 先判断是否左移/右移,同样,将方块现在所处位置的map_s中元素设为0,看其移动后的位置上map_s的元素是否有1,
# 若有,说明这一位置已被占据或已到边界,不可移动,返回False。若可移动,返回True。按下左键,若可
# 以移动,核心块的横坐标减1,由于我们只讨论其他小块对于核心块的相对位置,所以其他小块的位置自动随
# 核心块的位置移动而移动。将移动过后的位置对应map_s中的元素设为1。
def can_left(self):
core_now = core
for i in range(4):
map_s[((core_now[0] + getNew[i][0]), (core_now[1] + getNew[i][1]))] = 0
for i in range(4):
if map_s[((core_now[0] + getNew[i][0] - 1), (core_now[1] + getNew[i][1]))] == 1:
return False
return True
# 左移
def move_left(self, event):
if self.can_left():
core[0] -= 1
self.draw_square()
self.draw_bottom()
if not self.can_move():
for i in range(4):
map_s[((core[0] + getNew[i][0]), (core[1] + getNew[i][1]))] = 1
# 判断右移
def can_right(self):
for i in range(4):
map_s[((core[0] + getNew[i][0]), (core[1] + getNew[i][1]))] = 0
for i in range(4):
if map_s[((core[0] + getNew[i][0] + 1), (core[1] + getNew[i][1]))] == 1:
return False
return True
# 右移
def move_right(self, event):
if self.can_right():
core[0] += 1
self.draw_square()
self.draw_bottom()
if not self.can_move():
for i in range(4):
map_s[((core[0] + getNew[i][0]), (core[1] + getNew[i][1]))] = 1
# 初始化中有一self. isFaster的变量被设为False,当其为False时,
# 程序中的sleep(time)中time的值为0.35,而按下下键,self. isFaster变为True,
# time变成0.05,通过调整sleep()中变量的大小可以调节方块运动的速度。
# 此功能通过if语句实现。
def move_faster(self, event):
self.isFaster = True
if not self.can_move():
for i in range(4):
map_s[((core[0] + getNew[i][0]), (core[1] + getNew[i][1]))] = 1
# run the program
def run(self):
self.draw_bottom()
self.is_fill()
self.draw_new()
# play the game
def play(self):
self.sflag=True
self.startBut.config(state=DISABLED)
global is_pause
is_pause = True
global map_s
map_s = self.map_s
while True:
if self.is_over():
self.run()
else:
break
self.over()
# restart the game
def restart(self):
self.map_s = {}
for i in range(self.width):
for j in range(-4, self.height):
self.map_s[(i, j)] = 0
for i in range(-1, self.width):
self.map_s[(i, self.height)] = 1
for j in range(-4, self.height + 1):
self.map_s[(-1, j)] = 1
self.map_s[(self.width, j)] = 1
self.score = 0
for j in range(self.height):
self.area.delete('bottom' + str(j))
self.play()
# 结束后告诉用户失败
def over(self):
if self.sflag:
self.ARecord()
self.myRecord()
feedback = messagebox.askquestion("游戏结束!", "再次挑战?")
self.delmyRecord()
if feedback == 'yes':
self.restart()
else:
self.root.destroy()
# 退出
def is_quit(self):
ask_quit = messagebox.askquestion("退出", "保存并退出?")
if ask_quit == 'yes':
if self.sflag:
self.ARecord()
self.myRecord()
self.root.destroy()
exit()
# 判断是否按下restart
def is_restart(self):
ask_restart = messagebox.askquestion("放弃游戏", "确定重新开始?")
if ask_restart == 'yes':
self.delmyRecord()
if self.sflag:
self.ARecord()
self.restart()
else:
return
# 每次一按下暂停键,is_pause = not is_pause,当is_pause = True时,由于之前提到过的if is_pause:语句,
# 方块可以移动,游戏运行。当按下暂停键以后,is_pause值为False,方块将不可移动。同时,is_pause值为False时
# ,暂停键变为开始键,即标签由Pause改为Resume,当is_pause值为True时,Resume改为Pause。这一功能由if语句实现。
def is_pause(self):
global is_pause
is_pause = not is_pause
if not is_pause:
self.pauseBut["text"] = "继续"
else:
self.pauseBut["text"] = "暂停"
# 帮助
def rule(self):
rule_top = Toplevel()
rule_top.title('帮助')
rule_top.geometry('800x400')
rule = "按下开始进行游戏 使用‘w’‘a’‘s’‘d’或‘↑’‘↓’‘←’‘→’控制."
rule_label = Label(rule_top, text=rule, fg='blue', font=18)
rule_label.place(x=50, y=50)
# 显示有关信息
def about(self):
about_top = Toplevel()
about_top.title('关于')
about_top.geometry('300x150')
about = "俄罗斯方块."
about_label = Label(about_top, font=('Curier', 20), fg='darkblue', text=about)
about_label.pack()
#判断txt文件是否存在 不存在创建txt---------
def create_not_exist(self):
FileName1='Record.txt'
FileName2='G_Record.txt'
if not (os.path.exists(FileName1) and os.path.exists(FileName2)):
f1 = open(FileName1, mode="a", encoding="utf-8")
f2 = open(FileName2, mode="a", encoding="utf-8")
f1.close()
f2.close()
#存入记录---------
def ARecord(self):
f = open('Record.txt','a')
f.write(self.scoreLabel2['text'])
f.write(',')
f.close()
#排序获得最高的记录//---------
def getMax(self):
f = open('Record.txt')
rs=f.read().split(',')
if rs[0]=='':
maxs=['0']
else:
del rs[-1]
maxs=list(map(int,rs))
maxs.sort()
maxs.reverse()
f.close()
return maxs
#排行记录--------
def getSort(self):
sort=str(self.getMax())
ask_restart = messagebox.askquestion('排行','排行:{}'.format(sort))
#历史所有记录--------
def getRecord(self):
f = open('Record.txt')
rs=f.read()
ask_restart = messagebox.askquestion('历史记录','记录:{}'.format(rs))
f.close()
#窗口右上角叉掉提示//-------------
def my_close(self):
res = messagebox.askokcancel('提示', '是否关闭窗口')
if res == True:
if self.sflag:
self.ARecord()
self.myRecord()
self.root.destroy()
#删除上次记录从零开始
def delmyRecord(self):
f = open('G_Record.txt', mode="w", encoding="utf-8")
f.close()
self.scoreLabel2.config(text='0')
#写入上次数据 上次游戏的地图x(myk)与y(myv)坐标以及随机方块到达的位置(sq)/---------------------------
def myRecord(self):
f = open('G_Record.txt', mode="w", encoding="utf-8")
myk=str(self.map_s.keys())
myv=str(self.map_s.values())
#核心块
sq=str((core[0], core[1]))
#方块形状
rd=str(next_s)
#上次记录
rc=self.scoreLabel2['text']
date=myk+myv+'&'+sq+rd+','+rc
f.write(date)
f.close()
#读取上次数据/---------------------
def rmyRecord(self):
f = open('G_Record.txt', mode="r", encoding="utf-8")
read=f.read()
if len(read)==0:
kv={}
self.newflag=False
else:
#key value分界下标位置
n=read.index('v')-5
m=read.index('&')-1
#字典key坐标x,y,x,y....
listk=[int(k) for k in re.findall(r'-?\d+', read[11:n-2])]
#字典value
listvalue=[int(v) for v in re.findall(r'\d',read[(n+13):m-2])]
x=0
listkey=[]
i=1
for y in listk:
if (i%2)==0:
lk=(x,y)
listkey.append(lk)
else:
x=y
i+=1
kv=dict(zip(listkey,listvalue))
f.close()
return kv
#返回上次的方块(列表)核心坐标
def rmysq(self):
f = open('G_Record.txt', mode="r", encoding="utf-8")
read=f.read()
if len(read)==0:
mysq=[]
else:
n=read.index('&')
mysq=[int(k) for k in re.findall(r'-?\d+', read[n:])]
f.close()
return mysq
def mainloop(self):
self.root.protocol('WM_DELETE_WINDOW', self.my_close)
self.root.mainloop()
def main():
russia_block = RussiaBlock()
russia_block.mainloop()
main()
五、项目总结
5.1 特点
5.2 不足之处