一百行写一个2048

Posted 神仙盼盼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一百行写一个2048相关的知识,希望对你有一定的参考价值。

文章目录

零、写在之前

这个2048小游戏是后面为了用来做强化学习而写的游戏,这里并没有图形化界面,但后面会用pygame补充一下显示界面。
然后简单减少一下本次实验的环境:

  • pycharm2021
  • python3.9
  • 第三方库:numpy

然后让我们来看一下成果图

壹、设计思路

无论做什么,在前期都有必要做一个设计思路的概要,计时后面你发现他有很多不完善的地方,但是他却可以给你的设计带来逻辑性,并对大题的布局和整体代码架构有更加清晰的认知:

  1. 主程序模块:我们需要一个主程序来调用其他程序,保证整体代码架构的稳定;
  2. 展示模块:程序和人的交互是必不可缺的,在这里我们还没有准备图形界面,故交互方式是采用的命令行;
  3. 随即出生模块:随即出生模块将负责我们的新生成的元素出生在空位上,并且来判断程序是否结束;
  4. 移动模块:这部分是程序运行的主体,负责程序主要算法:如何移动方块,合并单元。

通过以上四个模块,我们可以实现一个用户界面的模块。在本文中包含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()

陆、移动模块

在移动模块中,我们需要考虑到如下问题:

  1. 功能如何实现?
  2. 是否可以实现优化呢?

关于2048游戏的算法,通过我大量实【you】验【xi】,总结如下:
方块会与移动方向最靠前的相似的合并,但合并后的不会再参与到这次的其他合并中去了,举个简单例子,在我移动方向上有4, 4, 4这样的组合,那么移动结果是8, 4, 而如果是2, 2, 4,那么移动结果就是4, 4,并不会合并成8。
在参考了网上的思路后,我大致总结了一下,移动分为三步:

  1. 首先把移动方向上,多余的空位去除,这将使得我所有有东西的数字都贴在一起;
  2. 那么接下来我移动方向上相同的元素合并,靠前位置的元素翻倍,靠后一位为零;这一步非常妙,这样原本我后面那个元素和合并后的元素即使可以合并,但因为空了一位,所以也无法合并了!
  3. 最后我们将多余的空格再次合并即可。

在这里我们不难发现第一步和第三步完全一样,所以最初我的代码是将四个移动方向全部分开实现,零零总总写了一百多行…
直到我写完我才意识到,这个移动的重复性很强!换一句话说,他们之间一定存在什么规律,我可以用什么方法合并成一个函数。
在我一天的沉思之下,我终于意识到了问题的关键之处:他都是在想移动的方向合并,换句话说,如果我把每次运算的矩阵先转个头,使得最后的移动方向都是朝上,那么我只需要一个朝上的函数去操作就可以了,在之后我只需要让这个矩阵再转回去,那么我就可以用一个函数解决这个问题!【感觉自己好棒!】

于是这部分的代码如下

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的主要内容,如果未能解决你的问题,请参考以下文章

一百行写一个2048

详解matlab之简易2048制作

忙活了一天,第一次写超过一百行的代码

JavaScript小游戏--2048(PC端)

CCF-棋局评估 201803-04(版本 2.0)------(之前写了一个臃肿的1.0版 ,还沾沾自喜 233)

三百行代码完成一个简单的rpc框架