python实现2048——1、界面&逻辑

前言
这次,我们来尝试一下2048这款游戏,不了解的可以自行玩一下,这里不展开了。
还是使用pygame,因为我们之前已经有过了好几个pygame项目了,这次我准备快一点,主要讲其中的逻辑部分。

界面搭建
这部分的pygame调用会一笔掠过,如果不知道原理的看一下这个:
之前的部分

2048的效果是这样的:

%title插图%num

4*4的格子,每一个格子上都有一个数字,并且下面有我们的得分。

这次我们没有再使用创建类的方式来存储rect对象和数字,而是分开成两个。
num二维数组:存储数据;
rectangle数组:存储rect对象。
然后将数字和一个个的方块对应上。

整个界面我们是渲染了背景颜色,然后对每一种可能出现的数字都赋予一种和背景颜色不同的颜色,并且我们的方块稍微窄一些,这样就能够显示出一个漂亮的分割线。
随后,在数字非零的方块上,我们显示出数字,就实现了功能。

“””2048小游戏”””
import pygame
from fuc import *
from time import sleep
# 游戏状态
“””当变量为0时,结束循环,退出游戏”””
game_stats = 1

# 颜色
“””背景颜色,深灰色”””
bg_color = (105, 105, 105)
“””每一种数字对应的rgb颜色,用字典封装方便查找”””
color = {
0 :(211, 211, 211),
2 :(255, 255, 255),
4 :(253, 245, 230),
8 :(255, 228, 196),
16 :(255, 222, 173),
32 :(255, 193, 193),
64 :(255, 106, 106),
128:(255, 255, 0 ),
256:(218, 165, 32),
512:(184, 134, 11)
}

# 格子的宽度、高度
lattice = 99

# 得分
score = 0

# 参数封装
“””参数封装成列表,方便我们进行变量的修改”””
game = [game_stats, score]

# 创建矩阵
“””问题1″””
num = [ [0]*4 for i in range(4)]
# rect对象数组,这个是一维数组!,而且这个xy顺序很烦的
“””问题2″””
rectangle = [pygame.Rect(x*lattice+x+1,y*lattice+y+1,lattice,lattice) for y in range(4) for x in range(4)]

# 创建screen对象
pygame.init()
screen = pygame.display.set_mode((400,430))
pygame.display.set_caption(‘2048’)

# 开始游戏
“””先生成一个,不然玩个鬼”””
new(num,game)
while game[0]:
“”刷新””””
update(screen,bg_color,color,num,rectangle,game)
“””事件检测”””
check_events(num,game)
update(screen,bg_color,color,num,rectangle,game)
sleep(3)

问题:

我们如果是使用[[0] *4] *4,你看着会生成一个4 * 4的矩阵,但是当你进行元素的修改,呵呵,原地爆炸。
首先,我们是使用[0]4生成了一个[0,0,0,0]的列表,每一个内容都是不可变类型,进行修改确实没什么,但是第二层,[列表]*4,实际上是将一个二维数组的每一行都指向同一个一维列表。而列表是可变类型,当你进行修改的时候,是在之前的基础上改变的,这样就会导致指向四个列表的位置全部改变。

%title插图%num
这里我采用了一个列表解析,外部的for循环只有一个计数的功能。
这里注意了,我们的rect对象是用一个一维列表存储的,别跟我一样用二维列表的方式调用然后崩了。
Rect函数参数:x、y坐标(左上角),以及矩形的宽度、高度
这里主要是逻辑比较烦,我想要的是0123 \n 4567这样的,所以需要先遍历y然后是x,这样我们第i行第j列的元素就可以使用4*i+j来调用了。
这里,我将处理函数添加在事件检测中了,在确定事件之后,我们就调用对应的函数。

在游戏的*后,我们刷新了一下屏幕,这是因为当判断游戏结束时,我们是没机会再次刷新的,所以会导致看到的结果是不完善的。(其实也可以将update放在check之后,反正玩家没有电脑快)

函数部分
接下来是逻辑部分。
这个游戏就上下左右的检测,然后是一个 关闭界面的退出。
在检测了事件之后,我们就需要判断移动是否有效,因为有效的移动就需要生成一个新的方块;
同时在每一次新生成方块之后,就可能会结束游戏,所以在生成之后我们还需要一个检测。

事件检测
def check_events(num,game):
for event in pygame.event.get():
“””退出事件的检测”””
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
“””按键事件”””
pass

然后我们用event.key == pygame.K_UP来判断按键的方向,类似的还有K_DOWN、K_LEFT、K_RIGHT这些。

就拿向上为例吧。
向上事件中,我们是将数字向上移动和合并,因为每一个方向都是不一样的,所以我进行了一个整合:

for j in range(4):
s = [num[i][j] for i in range(4)]
s_ret = my_sort(s,game)
for i in range(4):
num[i][j] = s_ret[i]

my_sort函数实现了将一个列表向0下标整理的操作,在向上的操作中,我们需要向上整理,所以传入列表推导式s,调用函数实现一列的移动和整合。
还有一个问题就是在我们成功的移动之后,就需要生成一个新的方块。这里的函数叫做new(),实现在0位置生成一个2或者4。那么问题就是如何判断是否成功移动。

ret = 0
for j in range(4):
s = [num[i][j] for i in range(4)]
s_ret = my_sort(s,game)
if s_ret == s:
ret += 1
else:
for i in range(4):
num[i][j] = s_ret[i]
if ret != 4:
new(num,game)

对于每一列,我们保存生成的列表和返回的列表,如果相同则说明该列没有移动成功,如果不同则说明有变化,移动成功。

整体:
(还是没法将四个方向进行一个综合,没想到太好的办法,希望dl指点)

def check_events(num,game):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
ret = 0
for j in range(4):
s = [num[i][j] for i in range(4)]
s_ret = my_sort(s,game)
if s_ret == s:
ret += 1
else:
for i in range(4):
num[i][j] = s_ret[i]
if ret != 4:
new(num,game)
elif event.key == pygame.K_DOWN:
ret = 0
for j in range(4):
s = [num[i][j] for i in range(3,-1,-1)]
s_ret = my_sort(s,game)
if s_ret == s:
ret += 1
else:
for i in range(4):
num[3-i][j] = s_ret[i]
if ret != 4:
new(num,game)
elif event.key == pygame.K_LEFT:
ret = 0
for i in range(4):
s = num[i]
s_ret = my_sort(s,game)
if s_ret == s:
ret += 1
else:
num[i] = s_ret
if ret != 4:
new(num,game)
elif event.key == pygame.K_RIGHT:
ret = 0
for i in range(4):
s = num[i][::-1]
s_ret = my_sort(s,game)
if s_ret == s:
ret += 1
else:
num[i] = s_ret[::-1]
if ret != 4:
new(num,game)

接下来只需要实现检查、排序和new即可。

生成
这个还算简单

def new(num,game):
“””生成新的方块”””
success = 1
x = y = 0
“””先检测一下是否能生成,一般来说用不上”””
for i in range(4):
for j in range(4):
if not num[i][j]:
success = 0
if success:
game[0] = 0
return
# 随机数判断生成啥
while not success:
x = randint(0,3)
y = randint(0,3)
if not num[x][y]:
if (x+y)%2:
num[x][y] = 2
else:
num[x][y] = 4
break
check(num,game)

检查函数
这部分,我也是想了好久,但是只能给出一个比较i复杂的,那就是四个方向都模拟一遍,然后判断是否为都失败了。

def check(num,game):
stats = 0
ret = 0
for i in range(4):
s = num[i]
s_ret = my_sort(s,game)
# 有一列没有移动成功
if s == s_ret:
ret += 1
# 四列都没有移动成功
if ret == 4:
stats += 1
ret = 0
for i in range(4):
s = num[i][::-1]
s_ret = my_sort(s,game)
if s == s_ret:
ret += 1
if ret ==4:
stats += 1
ret = 0
for j in range(4):
s = [num[i][j] for i in range(4)]
s_ret = my_sort(s,game)
if s == s_ret:
ret += 1
if ret == 4:
stats += 1
ret = 0
for j in range(4):
s = [num[3-i][j] for i in range(4)]
s_ret = my_sort(s,game)
if s == s_ret:
ret += 1
if ret == 4:
stats += 1
# 四个方向都废了,说明死掉了
if stats == 4:
game[0] = 0

全程计数,和我们的事件检测基本上相同,只是没有将数据赋值回去罢了。

排序
好了么,还是到这部分了,卡了我好几天的部分(其实是放假摸鱼太狠了)。
我整理了一下我们对于四个数字要实现的:

向下标0移动,中间不能有空着的
相邻的或者是中间只有0的两个元素,可以合并
合并时,下标小的*2,大的为0
合并*骚的一点是比如422这种,我们只能合并到44,而不是一个8,也就是参与合并之后的点不能接着合并了
我试过快慢指针来寻找两个元素,也试过while循环直到中间没有空格(复杂而且不满足第四点)

*后我得到了这样一个算法:
先将所有的非零元素拿出来,进行合并之后再处理一次0。
随后我们按照4个元素个数进行补0操作(因为是向0下标移动,所以是在后面补0)

def my_sort(s,game):
“””将需要处理的一行/一列拿出来,四个元素的列表
我们要把s[0]作为顶端”””
# 先处理,再合并
s = [i for i in s if i]
for i in range(1,len(s)):
if s[i-1] == s[i]:
s[i-1] *= 2
s[i] = 0
# game[1]是我们的得分,合并时可以一起加上
game[1] += s[i-1]
s = [i for i in s if i]
s += [0]*(4-len(s))
return s

*步去除0元素,我们只需要判断相邻的两个即可。

合并之后难面出现0,我们再去除一次,然后在后面补上即可。

刷新
补上这一部分,我们的游戏就能跑起来了。

def update(screen,bg_color,color,num,rectangle,game):
screen.fill(bg_color)
font = pygame.font.SysFont(None,48)
for i in range(4):
for j in range(4):
# 确定数字和rect对象
number = num[i][j]
the_rect = rectangle[i*4+j]
# 绘制底色
col = color[number]
pygame.draw.rect(screen,col,the_rect)
if number:
# 绘制数字
msg_image = font.render(str(number),True,(0, 0, 0),col)
msg_rect = msg_image.get_rect()
msg_rect.center = the_rect.center
screen.blit(msg_image,msg_rect)
# 绘制得分
string = “score:”
string += str(game[1])
msg_image = font.render(string,True,(255, 255, 255),bg_color)
screen.blit(msg_image,(0,400))
pygame.display.flip()

都是调用现成的pygame函数,不清楚的可以看一下往期的几个小游戏。
贪吃蛇
井字棋

结语
这样我们的整个游戏就完事了,不过我想到了几个优化点,我可能会在后续的博客中推出。

不过有一些我已经写过了,所以不准备再来一次了。

保存得分
如果是简单的,可以看这个:
txt读入、写入*高分
如果是高端一点的,想玩玩json文件的,看这个(主要是针对多个变量的,这里一个有一说一其实用不上)

如果是想多一点元素,比如在开始界面我们添加一个开始游戏按钮或者是在死亡之后添加一个弹窗显示得分和*高分:
看这个