一百行写一个2048
Posted 神仙盼盼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一百行写一个2048相关的知识,希望对你有一定的参考价值。
零、写在之前
这个2048小游戏是后面为了用来做强化学习而写的游戏,这里并没有图形化界面,但后面会用pygame补充一下显示界面。
然后简单减少一下本次实验的环境:
- pycharm2021
- python3.9
- 第三方库:numpy
然后让我们来看一下成果图
壹、设计思路
无论做什么,在前期都有必要做一个设计思路的概要,计时后面你发现他有很多不完善的地方,但是他却可以给你的设计带来逻辑性,并对大题的布局和整体代码架构有更加清晰的认知:
- 主程序模块:我们需要一个主程序来调用其他程序,保证整体代码架构的稳定;
- 展示模块:程序和人的交互是必不可缺的,在这里我们还没有准备图形界面,故交互方式是采用的命令行;
- 随即出生模块:随即出生模块将负责我们的新生成的元素出生在空位上,并且来判断程序是否结束;
- 移动模块:这部分是程序运行的主体,负责程序主要算法:如何移动方块,合并单元。
通过以上四个模块,我们可以实现一个用户界面的模块。在本文中包含numpy的基础知识【其实不太会也行】
贰、主程序的构建
首先我们来设计一下主程序,在主程序中,我们需要构建程序的大体框架,方便我们后面完成自顶向下的开发:
所以请让我们先来吧游戏中的主要参数罗列一下:
def main():
game_state = True # 标记游戏进行还是结束
while game_state:
# 参数区
reset = True # 复位信号,监督每一局的状态
score = 0 # 记录每局的分数
area = np.zeros((5, 5)) # 游戏矩阵
void_set = [] # 用来存放空余位置,方便生成函数定位
有了参数,我们接下来仅需要讲每一局游戏的状态罗列一下就可以了,所以主程序如下:
def main():
game_state = True # 标记游戏进行还是结束
while game_state:
# 参数区
reset = True # 复位信号,监督每一句的状态
score = 0 # 记录每局的分数
area = np.zeros((5, 5)) # 游戏矩阵
void_set = [] # 用来存放空余位置,方便生成函数定位
# 每局游戏状态
game_state, void_set = update_mat(area, void_set) # 开始游戏的首次更新
while game_state:
born(area, void_set) # 随即出生新的2or4
display(area, score) # 将当前地图展示
# 命令行键盘输入
direction = input("方向")
# 键盘检测输入
# direction = None
# Q学习输入
# direction = None
area, score = game_run(area, direction, score) # 对输入进行更新
game_state, void_set = update_mat(area, void_set) # 重新更新棋局状态
在这个上面我留下了pygame和AI打游戏的强化学习结构Q函数的输入。
叁、随机出生模块
随即出生函数是诞生新的元素的出生地,我们观察游戏可以发现,随即出生那个的元素为2或4,并且几乎保证等概率出现:
# 随机生成函数
def born(mat, void_list):
"""
void_list : 用来储存目前的2048矩阵中不包含元素的位置
mat : 用来储存2048的元素矩阵
"""
num = 2**random.randint(1, 2) # 选择新诞生的元素是2或者4
coordinates = void_list[random.randint(0, len(void_list)-1)] # 新添加位置坐标(x, y)
mat[coordinates[0]][coordinates[1]] = num # 将新诞生的num添加进矩阵(x, y)位置
return mat
肆、检查模块
在检查模块,我们需要对void_set
里的坐标进行重新的检查,所以这里我们每次需要将原来的void_set
清空,重新从矩阵中选择:
def update_mat(mat, void_list):
game_state = True
void_list.clear() # 首先清除所有元素
for i in range(mat.shape[0]): # 对每个area区域中的元素重新统计
for j in range(mat.shape[1]):
if mat[i][j] == 0:
void_list.append((i, j))
if len(void_list) == 0: # 如果最后没有空余的位置,则游戏结束
game_state = False
return game_state, void_list
伍、显示模块
显示代码主要用作游戏的结果输出显示,我们通过格式化的print
输出到命令行进行交互:
def display(mat, score):
print("当前得分:"+str(score))
for i in range(mat.shape[0]):
for j in range(mat.shape[0]):
print("%8d" % mat[i][j], end='')
print()
print()
陆、移动模块
在移动模块中,我们需要考虑到如下问题:
- 功能如何实现?
- 是否可以实现优化呢?
关于2048游戏的算法,通过我大量实【you】验【xi】,总结如下:
方块会与移动方向最靠前的相似的合并,但合并后的不会再参与到这次的其他合并中去了,举个简单例子,在我移动方向上有4, 4, 4
这样的组合,那么移动结果是8, 4
, 而如果是2, 2, 4
,那么移动结果就是4, 4
,并不会合并成8。
在参考了网上的思路后,我大致总结了一下,移动分为三步:
- 首先把移动方向上,多余的空位去除,这将使得我所有有东西的数字都贴在一起;
- 那么接下来我移动方向上相同的元素合并,靠前位置的元素翻倍,靠后一位为零;这一步非常妙,这样原本我后面那个元素和合并后的元素即使可以合并,但因为空了一位,所以也无法合并了!
- 最后我们将多余的空格再次合并即可。
在这里我们不难发现第一步和第三步完全一样,所以最初我的代码是将四个移动方向全部分开实现,零零总总写了一百多行…
直到我写完我才意识到,这个移动的重复性很强!换一句话说,他们之间一定存在什么规律,我可以用什么方法合并成一个函数。
在我一天的沉思之下,我终于意识到了问题的关键之处:他都是在想移动的方向合并,换句话说,如果我把每次运算的矩阵先转个头,使得最后的移动方向都是朝上,那么我只需要一个朝上的函数去操作就可以了,在之后我只需要让这个矩阵再转回去,那么我就可以用一个函数解决这个问题!【感觉自己好棒!】
于是这部分的代码如下
def del_none(mat):
# 去除多余零的函数,这里用了KMP的字符串匹配思想来降低时间复杂度
for i in range(mat.shape[0]):
zero, nzero = 0, 0 # zero指向最靠前的空位置, nzero指向zero所知位置后第一个非空位置
while nzero < mat.shape[1]:
while zero < mat.shape[1]:
if mat[zero][i] == 0: # 找到zero空的位置,等待替换
break
else:
zero += 1 # 非空就继续向后走
if mat[nzero][i] != 0 and zero < nzero: # 找到满足条件的位置,并开始替换
mat[zero][i], mat[nzero][i] = mat[nzero][i], mat[zero][i]
nzero += 1
return mat
def game_run(mat, dirction, score):
dirctions = {'w': 0, 'a': 1, 's': 2, 'd': 3}
mat = np.rot90(mat, 4-dirctions[dirction]) # 将所有的移动方向全部转换成向上移动
mat = del_none(mat) # 清楚移动方向上多余的0
for i in range(1, mat.shape[0]): # 所有元素检测相邻元素是否相同,相同合并
for j in range(mat.shape[1]):
if mat[i][j] != 0:
if mat[i - 1][j] == mat[i][j]:
mat[i - 1][j] += mat[i][j]
score += mat[i][j]
mat[i][j] = 0
mat = del_none(mat) # 再一次清楚多余的0
mat = np.rot90(mat, dirctions[dirction]) # 将矩阵转移回原来的样子【转了个圈】
return mat, score
柒、完全代码展示
import random
import numpy as np
def update_mat(mat, void_list):
game_state = True
void_list.clear() # 首先清除所有元素
for i in range(mat.shape[0]): # 对每个area区域中的元素重新统计
for j in range(mat.shape[1]):
if mat[i][j] == 0:
void_list.append((i, j))
if len(void_list) == 0: # 如果最后没有空余的位置,则游戏结束
game_state = False
return game_state, void_list
# 随机生成函数
def born(mat, void_list):
"""
void_list : 用来储存目前的2048矩阵中不包含元素的位置
mat : 用来储存2048的元素矩阵
"""
num = 2**random.randint(1, 2) # 选择新诞生的元素是2或者4
coordinates = void_list[random.randint(0, len(void_list)-1)] # 新添加位置坐标(x, y)
mat[coordinates[0]][coordinates[1]] = num # 将新诞生的num添加进矩阵(x, y)位置
return mat
def display(mat, score):
print("当前得分:"+str(score))
for i in range(mat.shape[0]):
for j in range(mat.shape[0]):
print("%8d" % mat[i][j], end='')
print()
print()
def del_none(mat):
# 去除多余零的函数,这里用了KMP的字符串匹配思想来降低时间复杂度
for i in range(mat.shape[0]):
zero, nzero = 0, 0 # zero指向最靠前的空位置, nzero指向zero所知位置后第一个非空位置
while nzero < mat.shape[1]:
while zero < mat.shape[1]:
if mat[zero][i] == 0: # 找到zero空的位置,等待替换
break
else:
zero += 1 # 非空就继续向后走
if mat[nzero][i] != 0 and zero < nzero: # 找到满足条件的位置,并开始替换
mat[zero][i], mat[nzero][i] = mat[nzero][i], mat[zero][i]
nzero += 1
return mat
def game_run(mat, dirction, score):
dirctions = {'w': 0, 'a': 1, 's': 2, 'd': 3}
mat = np.rot90(mat, 4-dirctions[dirction]) # 将所有的移动方向全部转换成向上移动
mat = del_none(mat) # 清楚移动方向上多余的0
for i in range(1, mat.shape[0]): # 所有元素检测相邻元素是否相同,相同合并
for j in range(mat.shape[1]):
if mat[i][j] != 0:
if mat[i - 1][j] == mat[i][j]:
mat[i - 1][j] += mat[i][j]
score += mat[i][j]
mat[i][j] = 0
mat = del_none(mat) # 再一次清楚多余的0
mat = np.rot90(mat, dirctions[dirction]) # 将矩阵转移回原来的样子【转了个圈】
return mat, score
def main():
game_state = True # 标记游戏进行还是结束
while game_state:
# 参数区
reset = True # 复位信号,监督每一句的状态
score = 0 # 记录每局的分数
area = np.zeros((5, 5)) # 游戏矩阵
void_set = [] # 用来存放空余位置,方便生成函数定位
# 每局游戏状态
game_state, void_set = update_mat(area, void_set) # 开始游戏的首次更新
while game_state:
born(area, void_set) # 随即出生新的2or4
display(area, score) # 将当前地图展示
# 命令行键盘输入
direction = input("方向")
# 键盘检测输入
# direction = None
# Q学习输入
# direction = None
area, score = game_run(area, direction, score) # 对输入进行更新
game_state, void_set = update_mat(area, void_set) # 重新更新棋局状态
if __name__ == "__main__":
main()
捌、一点感想
没想到之前的数学思维和一点点小算法【KMP】居然会在这里有用武之地,这里并没有详细的说KMP是因为在这里即使不用对时间复杂度的上升也不是很可怕。后续我将开始Qlearning的实现。最后再去做pygame的界面显示。这样Qlearning的训练可以和界面开发一起进行,将会使时间利用最大化。
以上是关于一百行写一个2048的主要内容,如果未能解决你的问题,请参考以下文章