翼度科技»论坛 编程开发 python 查看内容

【闲暇一写】用Python编写2048游戏(命令行版)

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
本篇博文围绕使用Python开发热门游戏2048 GAME(命令行版本)
代码未做任何优化(原生且随意)、全程以面向过程MVC的设计思想为主、开发环境是Ubuntu系统下的Pycharm
2048是我很久前学习Python过程中的一个作业,直入正题——
一、了解游戏

1. 介绍

2048》是一款单人在线和移动端游戏,由19岁的意大利人Gabriele Cirulli于2014年3月开发。游戏任务是在一个网格上滑动小方块来进行组合,直到形成一个带有有数字2048的方块(来源:维基百科
2. 玩法规则


  • 通过方向键让方块整体上下左右移动
  • 如果两个带有相同数字的方块在移动中碰撞,则它们会相加合并为一个新方块
  • 每次出现方块移动时,都会有一个值为2或者4的新方块出现
  • 初始开局时,4*4的方块,随机2个方块赋值2或者4
  • 其中所出现的数字都是2的幂,2,4,8,16......
二、MVC设计

Model:无
View:终端界面(有时间再研究一下pyQt),打印二维列表,输入输出控制
Controller:二维列表-矩阵、数据控制、上下左右操作、计分机制、方块合并处理等等
三、核心函数


通过观察游戏界面,可知数据由二维数组(线性代数--方阵)存储,将上图映射到如下代码:
  1. source = [
  2.     [0, 0, 0, 0],
  3.     [0, 0, 2, 2],
  4.     [0, 0, 0, 0],
  5.     [0, 0, 0, 0]
  6. ]
复制代码
通过玩法规则第1条可知,方向键上下左右移动,四种移动必然存在相似的操作,祁天暄讲师通过分析向左移动来书写后续代码,我这里也会以向左移动来分析如何写后续的代码。
  1. [0, 0, 2, 2]
复制代码
取上述一行,按左方向键移动后,可以看到两个方块2持续左移(如果左边还有非0的方块,那么就会顶住该非0的方块),然后相撞变成方块4,因为有方块移动,所以随机挑选一个方块0进行填充成2(不限于当前行,也可能发生在其他行):

本行规律:
  1. [0, 0, 2, 2] >发生滑动> [2, 2, 0, 0] >相等相撞求和> [4, 0, 0, 0] >随机填充> [4, 0, 0, 2]
复制代码
经过多局游戏结合上方的规律,可以得知以下规律:
  1. [0, 0, 2, 2] >发生滑动> [2, 2, 0, 0] >相等相撞求和> [4, 0, 0, 0] >随机填充> [4, 0, 0, 2]
  2. [4, 0, 2, 2] >发生滑动> [4, 2, 2, 0] >相等相撞求和> [4, 4, 0, 0] >随机填充> [4, 4, 0, 4]
  3. [4, 4, 2, 2] >发生滑动,不动> [4, 4, 2, 2] >相等相撞求和> [8, 4, 0, 0] >随机填充> [4, 0, 0, 2]
复制代码
1. 滑动处理

发生滑动的环节,可以得出一个规律,有0在非0元素的前方则必滑动,否则不动
  1. [0, 0, 2, 2] >发生滑动> [2, 2, 0, 0]
  2. [4, 0, 2, 2] >发生滑动> [4, 2, 2, 0]
  3. [4, 4, 2, 2] >发生滑动,不动> [4, 4, 2, 2]
复制代码
所以此处构造一个zero_to_end函数,功能就是将0移至末尾处,并保持非0元素应有的顺序,先看一下中规中矩的方式(冒泡式的移动,时间复杂度较高):
  1. def zero_to_end(list_data):
  2.     for i in range(3, -1, -1):
  3.         for j in range(i):
  4.             if list_data[j] == 0:
  5.                 list_data[j], list_data[j + 1] = list_data[j + 1], list_data[j]
复制代码
再看一下另一种写法(采用该函数,时间复杂度为O(n)):
  1. def zero_to_end(list_data):
  2.     """
  3.     重排序函数(核心算法)
  4.     非0元素移至最前(保持顺序),0元素移至最后,充当中间人处理列表的角色
  5.     :param list_data: list 一维列表
  6.     :return: None
  7.     """
  8.     for i in range(3, -1, -1):
  9.         if not list_data[i]:
  10.             del list_data[i]
  11.             list_data.append(0)
复制代码
2. 相等相撞求和

经上一函数,每一行列表都被处理成:若干非0元素有序在前,若干0元素在后
  1. [2, 2, 0, 0] >相等相撞求和> [4, 0, 0, 0]
  2. [4, 2, 2, 0] >相等相撞求和> [4, 4, 0, 0]
  3. [4, 4, 2, 2] >相等相撞求和> [8, 4, 0, 0]
复制代码
相等相撞求和这个过程肯定要统一函数处理,增加复用性,因此需要详细拆分该流程的细节:
  1. [4, 2, 2, 0]
  2. 如果第1个元素等==第2个元素:
  3.     则第1个元素 + 第2个元素,并赋给第1个元素的位置
  4.     删除第2个元素
  5.     末尾追加一个0
  6. [4, 2, 2, 0]
  7. 如果第2个元素==第3个元素(符合条件)
  8.     则第2个元素 + 第3个元素,并赋给第2个元素的位置[4, 4, 2, 0]
  9.     删除第3个元素[4, 2, 0]
  10.     末尾追加一个0[4, 2, 0, 0]
  11. [4, 2, 0, 0]
  12. 如果第3个元素==第4个元素
  13.     则第3个元素 + 第4个元素,并赋给第3个元素的位置
  14.     删除第4个元素
  15.     末尾追加一个0
复制代码
也就是说,相邻且相等的两个元素相加,应赋值给前方位置的元素,然后删除后方位置的元素,删除了一个,肯定还要凑回去的,根据游戏规则,补0即可
其实上方的逻辑还可以进行优化,当检测到当前位置的元素为0时,直接打断循环即可(因为已经被zero_to_end函数处理过了),封装成merge_single函数如下:
  1. def merge_single(list_data):
  2.     """
  3.     合并元素函数(核心算法)
  4.     重排序后,左边两个相邻相同的非0元素相加,后方补0,并加分(可diy)
  5.     如果两个相邻的元素不同或者为0,则不做其他操作
  6.     :param list_data: list 一维列表
  7.     :return: None
  8.     """
  9.     zero_to_end(list_data)  # 处理一维列表
  10.     for i in range(3):
  11.         if list_data[i] == 0: break  # 检测到当前位置为0,后方就不管了,直接打断
  12.         if list_data[i] == list_data[i + 1]:
  13.             list_data[i] *= 2  # 等价于 += list_data[i + 1]
  14.             del list_data[i + 1]  # 删除 [i + 1]位置的元素
  15.             list_data.append(0)  # 补0
复制代码
有点不太放心,放一条数据进行测试:
  1. [4, 4, 2, 2] >i = 0,相加> [8, 4, 2, 2] >删除i + 1位置> [8, 2, 2] >补0> [8, 2, 2, 0]
  2. >循环结束第1次,i = 1,又相加> [8, 4, 2, 0] >又删除i + 1位置> [8, 4, 0] >又补0> [8, 4, 0, 0]
  3. > 循环结束第2次,i = 2,发现是0,直接跳出循环
复制代码
3. 随机填充

玩法规则第3条,当游戏中,发生方块滑动时,在0元素区域随机抽取一个位置,随机赋值2或者4。
因此,先定义全局变量,一个存2和4的元组,通过random模块实现随机索引获取2或者4。
  1. random_tuple = (2, 4)  # 初始添加的值、移动时添加的值
复制代码
通过while循环不断寻找随机方格,直到发现该方格存储0,那么该方格将被赋予新值。
  1. def random_site():
  2.     """
  3.     随机填充0元素函数(非核心)
  4.     随机挑选0元素的位置,进行随机填充random_list中的任意一个元素
  5.     可通过增删改变random_list中的元素,从而影响到随机填充的数字
  6.     :return: None
  7.     """
  8.     random_list_len = len(random_tuple)
  9.     while True:
  10.         x = random.randint(0, 3)
  11.         y = random.randint(0, 3)
  12.         if after_source[x][y] == 0:
  13.             after_source[x][y] = random_tuple[random.randint(0, random_list_len - 1)]
  14.             break
复制代码
四、附加功能函数

通过以上三步,成功的完成了2048的核心功能,接下来逐一部署2048的初始化、游戏操作、用户操作、打印等等函数。
1. 初始数据和矩阵比较

构造游戏初始数据,以全局变量表示:
  1. score = 0  # 初始分数,后续累加即可
  2. source = [
  3.     [0, 0, 0, 0],
  4.     [0, 0, 0, 0],
  5.     [0, 0, 0, 0],
  6.     [0, 0, 0, 0]
  7. ]
复制代码
单纯的一个source表示数据可能还不够,我构造了两个4*4的矩阵,分别命名为before_source和after_source:
  1. before_source = [
  2.     [0, 0, 0, 0],
  3.     [0, 0, 0, 0],
  4.     [0, 0, 0, 0],
  5.     [0, 0, 0, 0]
  6. ]  # 操作前的矩阵
  7. after_source = [
  8.     [0, 0, 0, 0],
  9.     [0, 0, 0, 0],
  10.     [0, 0, 0, 0],
  11.     [0, 0, 0, 0]
  12. ]  # 操作后的矩阵(当前打印的矩阵)
复制代码
这两个矩阵,用于用户操作前的一个比较(后台比较),假设用户进行左向移动,那么移动前的数据传给before_source,移动后(即每一行调用merge_single函数后)的数据传给after_source,after_source才是用户需要看的,两者在后台进行比较后,倘若不同,则说明发生了“方块移动”,那么就要调用随机填充的函数,如果相同,说明没有方块滑动,则不可随机填充。假如对以下的数据发起左移操作后,是不存在元素移动的,即不会调用随机填充:
  1. [
  2.     [4, 0, 0, 0],
  3.     [0, 0, 0, 0],
  4.     [2, 4, 2, 0],
  5.     [8, 2, 0, 0]
  6. ]
复制代码
根据上述分析,构造比较函数compare_matrix:
  1. def compare_matrix():
  2.     """
  3.     二维数组比较
  4.     操作前后的二维数组(矩阵)进行比较
  5.     如果不相等,说明有元素可移动,当移动时调用random_site()函数
  6.     """
  7.     if not (before_source == after_source):
  8.         random_site()
复制代码
2. 矩阵数据打印

每次执行完移动操作后(无论是上下还是左右),肯定都要反馈给用户数据界面,因此需要构造打印矩阵的函数:
  1. def print_list():
  2.     """打印游戏过程中必看的矩阵信息"""
  3.     for single_list in after_source:
  4.         print(single_list)
复制代码
3. main入口(框架搭建)

调用程序总该需要一个入口,构造main函数。
循环开始阶段,通过global关键字操作全局变量before_source,此处需要注意:应使用深拷贝(需要导入copy模块),将当前的数据拷贝给before_source,如果采取浅拷贝,操作after_source后,before_source也会跟随变化,这样就导致before_source恒等于after_source。
上下左右以input输入(w 、s、a、d)来进行移动,n表示主动认输,q表示退出游戏。
每次执行完移动操作后,都需要进行反馈数据界面,所以循环末尾需要调用print_list函数和打印当前分数:
  1. def main():
  2.     """程序入口:初始化 + 输入 + 输出"""
  3.     while True:
  4.         global before_source
  5.         before_source = copy.deepcopy(after_source)
  6.         key = input("键入:")
  7.         if key == "a": pass
  8.         if key == "d": pass
  9.         if key == "w": pass
  10.         if key == "s": pass
  11.         if key == "n": pass
  12.         if key == "q": break  
  13.         print_list()
  14.             
  15.             
  16. main()  # 调用main函数,即正常游戏的入口
复制代码
当准备输入时,手动中止程序,会发现很烦人的红色报错:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlpgZPVR-1674483797911)(/home/ronan/.config/Typora/typora-user-images/image-20230123213355607.png)]
因此加上try和except简单的处理一下:
  1. def main():
  2.     """程序入口:初始化 + 输入 + 输出"""
  3.     while True:
  4.         try:
  5.             key = input("键入:")
  6.             global before_source
  7.             before_source = copy.deepcopy(after_source)
  8.             if key == "a": pass
  9.             if key == "d": pass
  10.             if key == "w": pass
  11.             if key == "s": pass
  12.             if key == "n": pass
  13.             if key == "q": break
  14.             print(f"当前分数:{score}")
  15.             print_list()
  16.         except KeyboardInterrupt:
  17.             break
  18.         
  19.             
  20.             
  21. main()  # 调用main函数,即正常游戏的入口
复制代码
4. 实现认输功能

构建forfeit函数,打印最终分数后,人为抛出KeyboardInterrupt异常,直接调到except执行break打断循环(为了不在打印最终得分后,执行后续两条语句):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JqFS4KKo-1674483797912)(/home/ronan/.config/Typora/typora-user-images/image-20230123214632123.png)]
  1. def forfeit():
  2.     """认输"""
  3.     print(f"玩家已认输,最终得分:{score}")
  4.     raise KeyboardInterrupt
复制代码
5. 加分机制

每发生方块碰撞合并后,相加数字即为当局加分,比如初始为0,两个相邻的方块2碰撞合并后变成方块4,当前分数+4,即分数为4。
在merge_single函数中(参考2.2的函数),list_data.append(0)语句后添加下述代码:
  1. global score
  2. score += list_data[i]
复制代码
即:
  1. def merge_single(list_data):
  2.     """
  3.     合并元素函数(核心算法)
  4.     重排序后,左边两个相邻相同的非0元素相加,后方补0,并加分(可diy)
  5.     如果两个相邻的元素不同或者为0,则不做其他操作
  6.     :param list_data: list 一维列表
  7.     :return: None
  8.     """
  9.     zero_to_end(list_data)
  10.     for i in range(3):
  11.         if list_data[i] == 0: break
  12.         if list_data[i] == list_data[i + 1]:
  13.             list_data[i] *= 2
  14.             del list_data[i + 1]
  15.             list_data.append(0)
  16.             global score
  17.             score += list_data[i]
复制代码
6. 游戏初始化

为了让游戏设置得灵活一点,增加全局变量init_count,表示初始方块需赋值2或者4的个数,通常为2。
  1. init_count = 2  # 初始值的个数
  2. def init():
  3.     """
  4.     游戏初始化
  5.     :return: None
  6.     """
  7.     print(f"""当前分数:{score}\n操作方式:q退出 n认输
  8.        w(上)
  9. a(左)  s(下)  d(右)""")
  10.     for i in range(init_count):  # 随机生成init_count个初始值
  11.         random_site()
  12.     print_list()
复制代码
7. 各方向移动操作

首先要明确,核心函数中第2节的相等相撞合并函数,仅仅针对一维列表,而整个游戏以二维列表为主,因此需要复用该代码:
  1. def merge():
  2.     """合并操作,详见merge_single()函数"""
  3.     for i in range(4):
  4.         merge_single(after_source[i])
复制代码
接下来逐一分析,左右上下操作如何实现...
(1)左(基础操作)

调用merge函数,完成滑动合并,然后比较前后矩阵相等来决定是否调用随机填充(即调用compare_matrix函数)
  1. def left():
  2.     """向左操作"""
  3.     merge()
复制代码
(2)右

最暴力无脑的办法就是复制上述函数代码,然后更改,但是我一直写这些简单清晰的函数,就是为了复用,所以这里应该想办法复用merge等代码,我以向左操作为基础,仅看merge函数,假设每一行都逆转,再进行向左的核心操作,再逆转回去,不就可以了吗,比如:
  1. [0, 2, 2, 4]向右移动操作后变成[0, 0, 4, 4]
  2. [0, 2, 2, 4] >逆转> [4, 2, 2, 0] >merge> [4, 4, 0, 0] >逆转> [0, 0, 4, 4]
复制代码
因此代码如下:
  1. def reverse():
  2.     """逆转2048二维列表中的每一行一维列表"""
  3.     for i in range(4):
  4.         after_source[i].reverse()
复制代码
  1. def right():
  2.     """向右操作"""
  3.     reverse()
  4.     merge()
  5.     reverse()
复制代码
(3)上

同样为了复用,以向左操作为基础,仅看merge函数,假设进行矩阵转置,然后调用merge操作,再转置,比如:
  1. [0, 2, 0, 0]
  2. [4, 2, 0, 0]
  3. [0, 0, 4, 0]
  4. [4, 0, 0, 0]
  5. 转置后
  6. [0, 4, 0, 4]
  7. [2, 2, 0, 0]
  8. [0, 0, 4, 0]
  9. [0, 0, 0, 0]
  10. 调用merge后
  11. [8, 0, 0, 0]
  12. [4, 0, 0, 0]
  13. [4, 0, 0, 0]
  14. [0, 0, 0, 0]
  15. 再转置回来,得到结果
  16. [8, 4, 4, 0]
  17. [0, 0, 0, 0]
  18. [0, 0, 0, 0]
  19. [0, 0, 0, 0]
复制代码
因此代码如下(3种转置均可):
  1. def transposition():
  2.     """二维列表转置(矩阵转置)"""
  3.     for x in range(4):
  4.         for y in range(x, 4):
  5.             after_source[x][y], after_source[y][x] = after_source[y][x], after_source[x][y]
  6. def transposition():
  7.     """二维列表转置(矩阵转置)"""
  8.     new_map = [list(item) for item in zip(*after_source)]
  9.     after_source.clear()
  10.     after_source.extend(new_map)
  11. def transposition():
  12.     """二维列表转置(矩阵转置)"""
  13.     new_map = [list(item) for item in zip(*after_source)]
  14.     after_source[:] = new_map
复制代码
  1. def up():
  2.     """向上操作"""
  3.     transposition()
  4.     merge()
  5.     transposition()
复制代码
(3)下

根据上移和右移操作所得的灵感,同样是以左移为基础操作。假设进行矩阵转置,逆转后,调用merge操作,再逆转,再转置,比如:
  1. [0, 2, 0, 0]
  2. [4, 2, 0, 0]
  3. [0, 0, 4, 0]
  4. [4, 0, 0, 0]
  5. 转置后
  6. [0, 4, 0, 4]
  7. [2, 2, 0, 0]
  8. [0, 0, 4, 0]
  9. [0, 0, 0, 0]
  10. 逆转每一行后
  11. [4, 0, 4, 0]
  12. [0, 0, 2, 2]
  13. [0, 4, 0, 0]
  14. [0, 0, 0, 0]
  15. 调用merge后
  16. [8, 0, 0, 0]
  17. [4, 0, 0, 0]
  18. [4, 0, 0, 0]
  19. [0, 0, 0, 0]
  20. 再逆转回来
  21. [0, 0, 0, 8]
  22. [0, 0, 0, 4]
  23. [0, 0, 0, 4]
  24. [0, 0, 0, 0]
  25. 再转置回来,得到结果
  26. [0, 0, 0, 0]
  27. [0, 0, 0, 0]
  28. [0, 0, 0, 0]
  29. [8, 4, 4, 0]
复制代码
因此代码如下:
  1. def down():
  2.     """向下操作"""
  3.     transposition()
  4.     reverse()
  5.     merge()
  6.     reverse()
  7.     transposition()
复制代码
五、最终代码
  1. """    2048 GAME"""import randomimport copyscore = 0  # 分数init_count = 2  # 初始值的个数before_source = [
  2.     [0, 0, 0, 0],
  3.     [0, 0, 0, 0],
  4.     [0, 0, 0, 0],
  5.     [0, 0, 0, 0]
  6. ]  # 操作前的矩阵
  7. after_source = [
  8.     [0, 0, 0, 0],
  9.     [0, 0, 0, 0],
  10.     [0, 0, 0, 0],
  11.     [0, 0, 0, 0]
  12. ]  # 操作后的矩阵(当前打印的矩阵)random_tuple = (2, 4)  # 初始添加的值、移动时添加的值# Controller层def zero_to_end(list_data):
  13.     """
  14.     重排序函数(核心算法)
  15.     非0元素移至最前(保持顺序),0元素移至最后,充当中间人处理列表的角色
  16.     :param list_data: list 一维列表
  17.     :return: None
  18.     """
  19.     for i in range(3, -1, -1):
  20.         if not list_data[i]:
  21.             del list_data[i]
  22.             list_data.append(0)def merge_single(list_data):
  23.     """
  24.     合并元素函数(核心算法)
  25.     重排序后,左边两个相邻相同的非0元素相加,后方补0,并加分(可diy)
  26.     如果两个相邻的元素不同或者为0,则不做其他操作
  27.     :param list_data: list 一维列表
  28.     :return: None
  29.     """
  30.     zero_to_end(list_data)
  31.     for i in range(3):
  32.         if list_data[i] == 0: break
  33.         if list_data[i] == list_data[i + 1]:
  34.             list_data[i] *= 2
  35.             del list_data[i + 1]
  36.             list_data.append(0)
  37.             global score
  38.             score += list_data[i]def random_site():
  39.     """
  40.     随机填充0元素函数(非核心)
  41.     随机挑选0元素的位置,进行随机填充random_list中的任意一个元素
  42.     可通过增删改变random_list中的元素,从而影响到随机填充的数字
  43.     :return: None
  44.     """
  45.     random_list_len = len(random_tuple)
  46.     while True:
  47.         x = random.randint(0, 3)
  48.         y = random.randint(0, 3)
  49.         if after_source[x][y] == 0:
  50.             after_source[x][y] = random_tuple[random.randint(0, random_list_len - 1)]
  51.             breakdef merge():
  52.     """合并操作,详见merge_single()函数"""
  53.     for i in range(4):
  54.         merge_single(after_source[i])def reverse():
  55.     """逆转2048二维列表中的每一行一维列表"""
  56.     for i in range(4):
  57.         after_source[i].reverse()def transposition():    """二维列表转置(矩阵转置)"""    for x in range(4):        for y in range(x, 4):            after_source[x][y], after_source[y][x] = after_source[y][x], after_source[x][y]def compare_matrix():
  58.     """
  59.     二维数组比较
  60.     操作前后的二维数组(矩阵)进行比较
  61.     如果不相等,说明有元素可移动,当移动时调用random_site()函数
  62.     """
  63.     if not (before_source == after_source):
  64.         random_site()def left():
  65.     """向左操作"""
  66.     merge()def right():
  67.     """向右操作"""
  68.     reverse()
  69.     merge()
  70.     reverse()def up():
  71.     """向上操作"""
  72.     transposition()
  73.     merge()
  74.     transposition()def down():
  75.     """向下操作"""
  76.     transposition()
  77.     reverse()
  78.     merge()
  79.     reverse()
  80.     transposition()# View层def init():    """    游戏初始化    :return: None    """    print(f"""当前分数:{score}\n操作方式:q退出 n认输       w(上)a(左)  s(下)  d(右)""")    for i in range(init_count):  # 随机生成init_count个初始值        random_site()    print_list()def print_list():
  81.     """打印游戏过程中必看的矩阵信息"""
  82.     for single_list in after_source:
  83.         print(single_list)def forfeit():
  84.     """认输"""
  85.     print(f"玩家已认输,最终得分:{score}")
  86.     raise KeyboardInterruptdef main():    """程序入口:初始化 + 输入 + 输出"""    init()    while True:        try:            global before_source            before_source = copy.deepcopy(after_source)            key = input("键入:")            if key == "a": left()            if key == "d": right()            if key == "w": up()            if key == "s": down()            if key == "n": forfeit()            if key == "q": break            compare_matrix()            print(f"当前分数:{score}")            print_list()        except KeyboardInterrupt:            breakmain()
复制代码
来源:https://www.cnblogs.com/qinyu6/p/17736405.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具