优化算法应用(二)自动化俄罗斯方块

这是好多年前做过的一个东西,现在用优化算法来实现一下。

一. 目标描述

俄罗斯方块大家应该都玩过。
这里的目标是设计一个模型让电脑计算判断一个方块到哪个位置最好,让电脑自己控制方块的平移旋转,并使游戏能够长时间运行下去。

二. 模型设计

1.俄罗斯方块类型

俄罗斯方块的所有类型如下图:


一共7种类型,19个不同的放置类型。

2.方块掉落位置判断

俄罗斯方块的地图,我copy了一个宽10,高20的。
  在此地图下,由于计算量不大,我们可以遍历每一个块下落后的位置来判断是否要在该处下落。


  如上图,对于一个确定的放置类型,我们可以从左到右依次遍历其下落后的位置,上图中,该方块一共有9个不同位置,加上旋转,一共有(9+8)*2 = 34种不同情况。


  上图显示了位置1,4,9的最终下落后的情况,作为人类可以明确知道位置9要优于位置1,4,现在要如何将这一信息传达给电脑,让电脑也将方块落在位置9呢?

3.方块位置优劣判断模型

Tips:这个是我自己的模型,仅供参考。
  判断优劣有几个指标
  1. 整体的高度height
  2. 各列高度差之和height_diff_sum
  3. 空格的数量 space_num
  4. 各行缺少方块数
  5. 消除行数


  在上图中,没有完整行需要消除,整体的高度为4,(第4列最高),空格数为3(第2,8,10列)各一个。
  高度差为右侧与左侧之差,分别为0,1,1,4,2,1,0,0,0,1,总共为10。
  各方缺少方块数为第一行缺4块,第二行缺1块,第三行却5块,第四行缺9块。为了方便计算我将其分为了两类:1:只缺一块的行;2:缺多块的行。
上面的方块可记为:

  1. 最大高度height = 4
  2. 高度差height_diff_sum = 10
  3. 空格数space_num = 3
  4. 缺一块行数space_1_row = 1
  5. 缺大于一块行数space_2_row = 3
  6. 完成行数complete = 0

接下来就可以使用这些数据来计算该方块的得分了。
  Score = w1* height+
  w2*height_diff_sum+
  w3*space_num+
  w4*space_1_row+
  w5*space_2_row+
  w6*complete
  对于每一个下落的方块,将其旋转平移然后下落,然后计算整体方块的得分,选取整体得分最少的位置落下该方块,然后循环往复。


三. 优化算法结合

有了上面的模型,就可以明确优化算法所要优化的参数即:w1-w6这6个参数。
  那么构建的适应度函数的输入则是一个6维的向量。然而输出是什么呢?输出当然是从游戏开始到目前为止所消除的总行数。
  现在问题来了,对于每一次游戏,出现的方块是随机的,这就意味着,该适应度函数,即使确定了输入,也无法得到固定的输出
  不过这也不是大问题,虽然是随机,但肯定有一定的概率分布,我们可以每次让电脑进行多次游戏,然后取平均值,来减弱随机的干扰。我更过分一点,将多次游戏的最低得分作为适应度函数的输出。
  那么优化算法的适应度函数计算过程如下图:

由于每计算一次适应度函数,都要进行数局游戏,随着参数越来越优,游戏得分越来越高,每局游戏的时间也会越来越长。这会导致在算法前期,每一次迭代的运行速度很快,但是随着参数的优化,每次迭代所需的时间会越来越长。我们最终的目标是游戏能够无限玩下去,那么优化算法的运行时间也会接近无限长。
  为了缩短算法的运行时间,我给模型添加了一点限制,减少游戏整体的高度。正常游戏为宽10,高20;优化的时候,使用宽10高10来进行计算。这样能够缩短优化算法的运行时间,虽然对结果会有些许影响,但应该问题不大。

四. 代码实现

实现算法之前要先有一个俄罗斯方块游戏,在网上copy了一个,改造了一下。这次的代码将使用python实现。所需要的库pygame和其他的计算库请自行搜索安装。

1. 正常的俄罗斯方块游戏

import random
import sys
import pygame


class GameTetris:
    block_I = [[0, -2], [0, -1], [0, 0], [0, 1]]  # 0
    block_I_1 = [[-2, 0], [-1, 0], [0, 0], [1, 0]]  # 1
    block_O = [[0, 0], [0, 1], [1, 1], [1, 0]]  # 2
    block_S = [[0, -1], [0, 0], [-1, 0], [-1, 1]]  # 3
    block_S_1 = [[-1, -1], [0, -1], [0, 0], [1, 0]]  # 4
    block_Z = [[0, 1], [0, 0], [-1, 0], [-1, -1]]  # 5
    block_Z_1 = [[-1, 1], [0, 1], [0, 0], [1, 0]]  # 6
    block_T = [[0, 0], [0, 1], [1, 0], [-1, 0]]  # 7
    block_T_1 = [[0, 0], [0, 1], [1, 0], [0, -1]]  # 8
    block_T_2 = [[0, 0], [1, 0], [0, -1], [-1, 0]]  # 9
    block_T_3 = [[0, 0], [0, 1], [-1, 0], [0, -1]]  # 10
    block_J = [[0, 0], [1, 0], [-1, 0], [1, -1]]  # 11
    block_J_1 = [[0, 0], [0, -1], [0, 1], [1, 1]]  # 12
    block_J_2 = [[0, 0], [1, 0], [-1, 0], [-1, 1]]  # 13
    block_J_3 = [[0, 0], [0, 1], [0, -1], [-1, -1]]  # 14
    block_L = [[0, 0], [1, 0], [-1, 0], [1, 1]]  # 15
    block_L_1 = [[0, 0], [0, -1], [0, 1], [-1, 1]]  # 16
    block_L_2 = [[0, 0], [1, 0], [-1, 0], [-1, -1]]  # 17
    block_L_3 = [[0, 0], [0, 1], [0, -1], [1, -1]]  # 18
    all_block = [block_I, block_I_1,
                 block_O,
                 block_S, block_S_1,
                 block_Z, block_Z_1,
                 block_T, block_T_1, block_T_2, block_T_3,
                 block_J, block_J_1, block_J_2, block_J_3,
                 block_L, block_L_1, block_L_2, block_L_3
                 ]
    # 旋转用
    rotate_cur_id = [1, 2, 4, 6, 10, 14, 18]
    # 如果是上面的id则旋转后为下面的id,否则直接+1
    rotate_next_id = [0, 1, 3, 5, 7, 11, 15]

    # 方块边长
    block_size = 25
    # 窗口尺寸
    window_with = 16
    window_hight = 20
    # 地图尺寸
    map_height = 21
    map_width = 10

    gameover = False
    pause = False
    press = False
    times = 0
    score = 0

    # 方块地图
    map_block = None

    # 当前方块id
    cur_id = 0
    # 下一个方块的id
    next_id = 0
    cur_block = None
    next_block = None
    # 方块的初始位置
    block_initial_position = None
    screen = None

    # 初始化数据
    def init_data(self):
        self.gameover = False
        self.pause = False
        self.press = False
        self.times = 0
        self.score = 0
        # 当前方块id
        self.cur_id = 0
        # 下一个方块的id
        self.next_id = 0
        # 方块的初始位置
        self.block_initial_position = [self.map_height - 2, int(self.map_width / 2)]

        # 新建空白方块地图
        self.map_block = [[0 for column in range(0, self.map_width)] for row in range(0, self.map_height)]

        # 初始化当前方块和下一个方块
        self.next_id = random.randint(0, 18)
        self.next_block = self.all_block[self.next_id].copy()
        self.cur_id = random.randint(0, 18)
        self.cur_block = self.all_block[self.cur_id].copy()

    # 下落、位置、数组检测、得分、屏幕信息
    def block_move_down(self):
        y_drop = self.block_initial_position[0]
        x_move = self.block_initial_position[1]
        y_drop -= 1

        for row, column in self.cur_block:
            row += y_drop
            column += x_move
            # 如果要下降的位置已有方块,则跳出
            if row < 0:
                break
            if self.map_block[row][column] == 1:
                break
        else:
            # 如果要下降的位置没有方块则移动到该位置
            self.block_initial_position.clear()
            self.block_initial_position.extend([y_drop, x_move])
            # 没有到底
            return False
        return True

    # 计算有没有完成的行
    def check_complete(self):
        y_drop, x_move = self.block_initial_position

        # 将当前块的位置加入地图
        for row, column in self.cur_block:
            self.map_block[y_drop + row][x_move + column] = 1

        complete_row = []

        # 计算地图中是否有完成的行
        for row in range(0, self.map_height - 1):
            if 0 not in self.map_block[row]:
                complete_row.append(row)

        complete_row.sort(reverse=True)

        # 将完成的行消除
        for row in complete_row:
            self.map_block.pop(row)
            self.map_block.append([0 for column in range(0, self.map_width)])
        # 计算得分
        self.score += len(complete_row)
        # 将下一块复制到当前块
        self.cur_id = self.next_id
        self.cur_block = self.all_block[self.cur_id].copy()
        # 随机生成新的下一块
        self.next_id = random.randint(0, 18)
        self.next_block = self.all_block[self.next_id].copy()

        # 将新的方块的起始位置还原到中上
        self.block_initial_position.clear()
        self.block_initial_position.extend([self.map_height - 2, int(self.map_width / 2)])
        y_drop, x_move = self.block_initial_position
        for row, column in self.cur_block:
            row += y_drop
            column += x_move

            if self.map_block[row][column]:
                self.gameover = True
        # 不再继续下落
        return False

    # 方块的移动,防止出界,碰撞
    def move_left_right(self, n):
        y_drop, x_move = self.block_initial_position
        x_move += n
        for row, column in self.cur_block:
            row += y_drop
            column += x_move
            if column < 0 or column > self.map_width - 1 or self.map_block[row][column]:
                break
        else:
            self.block_initial_position.clear()
            self.block_initial_position.extend([y_drop, x_move])

    # 旋转,位置都进行变化
    def rotate(self,):
        if self.cur_id not in self.rotate_cur_id:
            self.cur_id = self.cur_id + 1
        else:
            for i in range(0, len(self.rotate_cur_id)):
                if self.cur_id == self.rotate_cur_id[i]:
                    self.cur_id = self.rotate_next_id[i]
                    break

        rotating_position = self.all_block[self.cur_id].copy()

        y_drop, x_move = self.block_initial_position
        for row, column in rotating_position:
            row += y_drop
            column += x_move
            if column < 0 or column > self.map_width - 1 or self.map_block[row][column]:
                break
        else:
            self.cur_block.clear()
            self.cur_block.extend(rotating_position)

    def draw_grid(self):
        color = (200, 200, 200)
        # 绘制网格
        # 纵向
        for i in range(0, self.map_width + 1):
            pygame.draw.line(self.screen, color, (i * self.block_size, 0),
                             (i * self.block_size, self.window_hight * self.block_size), 1)
        # 横向
        for i in range(0, self.map_height + 1):
            pygame.draw.line(self.screen, color, (0, i * self.block_size),
                             (self.map_width * self.block_size, i * self.block_size), 1)

        # 左右分隔线
        pygame.draw.line(self.screen, (0, 0, 0), (self.map_width * self.block_size, 0),
                         (self.map_width * self.block_size, self.window_hight * self.block_size), 2)

        # 绘制右边下一块的网格
        for i in range(1, 6):
            pygame.draw.line(self.screen, color, ((self.map_width + i) * self.block_size, (1) * self.block_size),
                             ((self.map_width + i) * self.block_size, (5) * self.block_size), 1)

        for i in range(1, 6):
            pygame.draw.line(self.screen, color, ((self.map_width + 1) * self.block_size, i * self.block_size),
                             ((self.map_width + 5) * self.block_size, i * self.block_size), 1)

    # 方块设置、变化、背景改变
    def update_screen(self):
        # 绘制当前块
        y_drop, x_move = self.block_initial_position
        for row, column in self.cur_block:
            row = row + y_drop
            column += x_move
            pygame.draw.rect(self.screen, (255, 165, 0), (
                column * self.block_size + 1, (self.map_height - row - 2) * self.block_size + 1, self.block_size - 1,
                self.block_size - 1))

        # 绘制下一块
        for row, column in self.next_block:
            pygame.draw.rect(self.screen, (255, 165, 0), (
                (self.map_width + column + 3) * self.block_size + 1, (2-row) * self.block_size + 1, self.block_size - 1,
                self.block_size - 1))

        # 如果方块掉落到底则改变颜色
        for row in range(0, self.map_height - 1):
            for column in range(0, self.map_width):
                bottom_block = self.map_block[row][column]
                if bottom_block:
                    pygame.draw.rect(self.screen, (0, 0, 255), (
                        column * self.block_size + 1, (self.map_height - row - 2) * self.block_size + 1,
                        self.block_size - 1, self.block_size - 1))

    def run(self):
        self.init_data()
        pygame.init()
        self.screen = pygame.display.set_mode((self.window_with * self.block_size, self.window_hight * self.block_size))

        pygame.display.set_caption("俄罗斯方块")

        while True:
            self.screen.fill((255, 255, 255))
            self.draw_grid()
            pygame.display.set_caption(str(self.score) + '分')
            if self.pause:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        sys.exit()
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                        self.pause = (~self.pause)
            if not self.pause:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        sys.exit()
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_LEFT:
                        self.move_left_right(-1)
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
                        self.move_left_right(1)
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_UP:
                        self.rotate()
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_DOWN:
                        self.press = True
                    elif event.type == pygame.KEYUP and event.key == pygame.K_DOWN:
                        self.press = False
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                        self.pause = (~self.pause)

                if self.press:
                    self.times += 10

                if self.times >= 50:
                    is_bottom = self.block_move_down()
                    if is_bottom:
                        self.check_complete()
                    self.times = 0
                else:
                    self.times += 1

                if self.gameover:
                    sys.exit()

                self.update_screen()
                pygame.time.Clock().tick(200)
                pygame.display.flip()


if __name__ == '__main__':
    game = GameTetris()
    game.run()

这是一个正常的俄罗斯方块,运行后效果如下:

2. 差分进化算法python实现

实现基本与matlab版本结构一致:

文件名 文件描述
Unit.py 个体基类
Algorithm_Impl.py 算法基类
DE_Unit.py 差分进化算法个体
DE_Base.py 差分进化算法基础实现
DE_Impl.py 差分进化算法实现
# 个体基类
class Unit:
   dim = 0
   position = None
   value = 0

   def __init__(self, dim):
       self.dim = dim
       self.position = [0]*dim
# 优化算法基类
import sys
import numpy as np


class Algorithm_Impl:
    # 当前最优位置
    position_best = None
    # 当前最优适应度
    value_best = - sys.float_info.max
    # 历史最优适应度
    value_best_history = list()
    # 是否为求最大值, 默认为是
    is_cal_max = True
    # 适应度函数,需要单独传入
    fitfunction = None
    # 调用适应度函数次数
    cal_fit_num = 0

    # 维度
    dim = 1
    # 种群中个体的数量
    size = 1
    # 最大迭代次数
    iter_max = 1
    # 解空间下界
    range_min_list = list()
    # 解空间上界
    range_max_list = list()
    # 种群列表
    unit_list = list()

    # 构造函数
    def __init__(self, dim, size, iter_max, range_min_list, range_max_list):
        self.size = size
        self.dim = dim
        self.iter_max = iter_max
        self.range_min_list = range_min_list
        self.range_max_list = range_max_list
        # 默认为求最大值
        self.is_cal_max = True

    # 初始化算法中的个体
    def init_unit(self):
        self.position_best = np.zeros((1, self.dim))[0]
        self.value_best_history = []
        # 设置初始最优值,由于是求最大值,所以设置了最大浮点数的负值
        self.value_best = - sys.float_info.max
        self.unit_list.clear()
        # for s in range(self.size):
        #     unit = Unit(self.dim)
        #     unit.position = np.random.rand((1, self.dim)).dot(
        #         self.range_max_list - self.range_min_list) + self.range_min_list
        #     unit.value = self.fitfunction(params=unit.position)
        #     self.unit_list.append(unit)

    # 计算适应度函数
    def cal_fitfunction(self, position=None):
        if position is None:
            return 0
        if self.fitfunction is None:
            return 0
        return self.fitfunction(params=position)

    # 设置适应度函数
    def set_fitfunction(self, fit_function):
        self.fitfunction = fit_function

    # 运行入口
    def run(self):
        self.init_unit()
        self.iteration()
        return

    # 循环迭代
    def iteration(self):
        for i in range(self.iter_max):
            self.update(i)
        return

    # 更新一次迭代
    def update(self, iter):
        # 记录最优值
        for i in range(self.size):
            if self.unit_list[i].value > self.value_best:
                self.value_best = self.unit_list[i].value
                self.position_best = self.unit_list[i].position
        print('第', iter, '代')
        if self.is_cal_max:
            self.value_best_history.append(self.value_best)
            print('最优值=', self.value_best)
        else:
            self.value_best_history.append(-self.value_best)
            print('最优值=', -self.value_best)
        print('最优解=', self.position_best.tolist())
        return

    # 某一维度越界值处理
    def get_out_bound_value_one(self, d, value):
        if value > self.range_max_list[d]:
            value = self.range_max_list[d]

        if value < self.range_min_list[d]:
            value = self.range_min_list[d]

        return value

    # 全部值越界处理
    def get_out_bound_value(self, value):
        for d in range(self.dim):
            if value[d] > self.range_max_list[d]:
                value[d] = self.range_max_list[d]

            if value[d] < self.range_min_list[d]:
                value[d] = self.range_min_list[d]
        return value
# 差分进化算法个体
from optimization_algorithm.frame.Unit import Unit


class DE_Unit(Unit):
    # 个体的新位置(变异位置)
    position_new = None

    def __init__(self, dim):
        super().__init__(dim)
# 差分进化算法
import copy
import random
import numpy as np

from optimization_algorithm.algorithm_differential_evolution.DE_Unit import DE_Unit
from optimization_algorithm.frame.Algorithm_Impl import Algorithm_Impl


class DE_Base(Algorithm_Impl):
    # 交叉概率
    cross_rate = 0.3
    # 变异概率
    alter_factor = 0.5

    # 初始化算法中的个体
    def init_unit(self):
        super().init_unit()

        for s in range(self.size):
            unit = DE_Unit(self.dim)
            unit.position = np.random.rand(1, self.dim)[0]*(
                    self.range_max_list - self.range_min_list) + self.range_min_list
            unit.value = self.fitfunction(params=unit.position)
            self.unit_list.append(unit)

    # 更新
    def update(self, i):
        super(DE_Base, self).update(i)
        self.altered()
        self.cross()
        self.choose()

    # 变异
    def altered(self):
        for s in range(self.size):
            # 生成3个不重复的随机数
            randList = random.sample(range(0, self.size), 3)
            new_position = self.unit_list[randList[0]].position + self.alter_factor * (
                    self.unit_list[randList[1]].position - self.unit_list[randList[2]].position)
            new_position = self.get_out_bound_value(new_position)
            self.unit_list[s].position_new = copy.deepcopy(new_position)

    # 交叉
    def cross(self):
        for s in range(self.size):
            rnbr = random.randint(0, self.dim)
            for d in range(self.dim):
                rnd = random.uniform(0.0, 1.0)
                if rnd > self.cross_rate and rnbr != d:
                    self.unit_list[s].position_new[d] = self.unit_list[s].position[d]
            self.unit_list[s].position_new = self.get_out_bound_value(self.unit_list[s].position_new)

    # 选择
    def choose(self):
        for s in range(self.size):
            new_value = self.cal_fitfunction(self.unit_list[s].position_new)
            if new_value > self.unit_list[s].value:
                self.unit_list[s].position = copy.deepcopy(self.unit_list[s].position_new)
                self.unit_list[s].value = new_value

            if new_value > self.value_best:
                self.position_best = copy.deepcopy(self.unit_list[s].position)
                self.value_best = new_value
# 差分进化算法实现
from optimization_algorithm.algorithm_differential_evolution.DE_Base import DE_Base


class DE_Impl(DE_Base):

    def __init__(self, dim, size, iter_max, range_min_list, range_max_list):
        super().__init__(dim, size, iter_max, range_min_list, range_max_list)
# 测试脚本
import numpy as np

from optimization_algorithm.algorithm_differential_evolution.DE_Base import DE_Base

if __name__ == '__main__':
    def fit_function(**kwargs):
        params = kwargs['params']
        if params is None:
            params = []
        result = 0
        for d in range(len(params)):
            result += params[d] * params[d]
        return -result


    ## 算法实例
    # 维度
    dim = 30
    # 种群数量
    size = 60
    # 最大迭代次数
    iter_max = 1000
    # 取值范围上界
    range_max_list = np.ones((1, dim))[0] * 100
    # 取值范围下界
    range_min_list = np.ones((1, dim))[0] * -100

    # 实例化差分进化算法类
    base = DE_Base(dim, size, iter_max, range_min_list, range_max_list)
    base.is_cal_max = False
    # 确定适应度函数
    base.fitfunction = fit_function
    # 运行
    base.run()
    print(base.value_best)
    print(base.position_best)

3. 使用优化算法进行优化

4. 自动化俄罗斯方块

这两个部分放在一起,因为3中的结果将成为4中的输入,可以分布进行,也可以顺序进行

import sys

import pygame

from game.tetris.GameTetris import GameTetris
from optimization_algorithm.algorithm_differential_evolution.DE_Base import DE_Base
import numpy as np

class GameTetrisAuto(GameTetris):
    rotate_num = 0
    x_offset = 0
    y_offset = 0

    weight_params = []

    top = GameTetris.map_height - 2

    score_min = 99999

    # 检查是否有整行,是否结束游戏
    def check_complete(self):
        GameTetris.check_complete(self)
        if 1 in self.map_block[self.top]:
            self.gameover = True
        self.rotate_num, self.x_offset = self.get_x_rotate_num(self.map_block, self.cur_id)

    def run_auto(self):
        self.init_data()
        pygame.init()
        self.screen = pygame.display.set_mode((self.window_with * self.block_size, self.window_hight * self.block_size))

        pygame.display.set_caption("俄罗斯方块")

        self.rotate_num, self.x_offset = self.get_x_rotate_num(self.map_block, self.cur_id)

        while True:
            self.screen.fill((255, 255, 255))
            self.draw_grid()
            pygame.display.set_caption(str(self.score) + '分')
            if self.gameover:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        sys.exit()
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                        self.pause = (~self.pause)
            else:
                if self.pause:
                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:
                            sys.exit()
                        elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                            self.pause = (~self.pause)
                if not self.pause:
                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:
                            sys.exit()
                        elif event.type == pygame.KEYDOWN and event.key == pygame.K_LEFT:
                            self.move_left_right(-1)
                        elif event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
                            self.move_left_right(1)
                        elif event.type == pygame.KEYDOWN and event.key == pygame.K_UP:
                            self.rotate()
                        elif event.type == pygame.KEYDOWN and event.key == pygame.K_DOWN:
                            self.press = True
                        elif event.type == pygame.KEYUP and event.key == pygame.K_DOWN:
                            self.press = False
                        elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                            self.pause = (~self.pause)

                    if self.rotate_num > 0:
                        # 旋转一次
                        self.rotate()
                        self.rotate_num = self.rotate_num - 1
                    if self.x_offset > 0:
                        # 右移一次
                        self.x_offset = self.x_offset - 1
                        self.move_left_right(1)
                    if self.x_offset < 0:
                        # 左移一次
                        self.x_offset = self.x_offset + 1
                        self.move_left_right(-1)

                    is_bottom = self.block_move_down()
                    if is_bottom:
                        self.check_complete()

                    if self.gameover:
                        # 游戏结束后不退出
                        # sys.exit()
                        continue

                    self.update_screen()
                    pygame.time.Clock().tick(200)
                    pygame.display.flip()

    # 计算当前地图和当前方块能得到的最优地图
    def get_x_rotate_num(self, map_block, cur_id):
        # 获取当前方块的id及其旋转后可能的形状
        temp_id = cur_id
        id_list = list()
        id_list.append(temp_id)
        for i in range(0, 4):
            if temp_id not in self.rotate_cur_id:
                temp_id = temp_id + 1
            else:
                for j in range(0, len(self.rotate_cur_id)):
                    if temp_id == self.rotate_cur_id[j]:
                        temp_id = self.rotate_next_id[j]
                        break
            if temp_id == cur_id:
                break
            id_list.append(temp_id)

        value_min = 1000
        res_id = 0
        x_move = 0
        # 遍历该方块的旋转后的方块
        for i in id_list:
            cur_block = self.all_block[i]
            value, x_offset = self.get_block_down_map(map_block, cur_block)
            if value < value_min:
                value_min = value
                res_id = i
                x_move = x_offset

        rotate_num = 0
        # 计算需要旋转多少次
        for i in id_list:
            if i == res_id:
                break
            else:
                rotate_num = rotate_num + 1
        return rotate_num, x_move

    # 根据当前地图和当前方块,计算掉落后的值
    def get_block_down_map(self, map_block, cur_block):
        init_position = [self.map_height - 2, int(self.map_width / 2)]
        y_pos, x_pos = init_position

        value_min = 9999999
        x_offset = 0

        # 遍历每一个宽度
        for i in range(int(-self.map_width / 2), int(self.map_width / 2)):

            # 判断x坐标上是否超出边界
            is_out_width = False
            for row, column in cur_block:
                x = column + (x_pos + i)
                if x < 0 or x > self.map_width - 1:
                    is_out_width = True
                    break
            if is_out_width:
                # 超出边界则跳过
                continue

            x_move, y_move = 0, 0
            # 复制当前地图
            temp_map_block = [[x for x in y] for y in map_block]

            # 遍历每一个高度
            is_bottom = False
            for j in range(1, self.map_height):
                for row, column in cur_block:
                    x_move = x_pos + i
                    y_move = y_pos - j
                    x = column + x_move
                    y = row + y_move

                    # 如果要下降的位置已有方块,则跳出
                    if y < 0:
                        is_bottom = True
                        break
                    if temp_map_block[y][x] == 1:
                        is_bottom = True
                        break

                if is_bottom:
                    y_move = y_move + 1
                    break
            if y_move + 2 >= self.map_height:
                break
            # 将当前块的位置加入地图
            for row, column in cur_block:
                temp_map_block[y_move + row][x_move + column] = 1

            value = self.cal_map_value(temp_map_block)
            if value < value_min:
                value_min = value
                x_offset = i
        # 返回该组合的值,和x方向偏移量
        return value_min, x_offset

    # 根据当前map_block计算评分
    def cal_map_value(self, map_block):
        weight_space = self.weight_params[0]
        weight_height_diff = self.weight_params[1]
        weight_complete = self.weight_params[2]
        weight_hight_max = self.weight_params[3]
        weight_space_row_1 = self.weight_params[4]
        weight_space_row_2 = self.weight_params[5]

        space = 0
        height_diff = 0
        complete = 0
        hight_max = 0
        space_row_1 = 0
        space_row_2 = 0

        complete_row = []

        # 计算地图中是否有完成的行
        for row in range(0, self.map_height - 1):
            if 1 in map_block[row] and row > hight_max:
                hight_max = row

            if 0 not in map_block[row]:
                complete_row.append(row)
                complete = complete + 1

        complete_row.sort(reverse=True)

        # 将完成的行消除
        for row in complete_row:
            map_block.pop(row)
            map_block.append([0 for column in range(0, self.map_width)])

        # 每一列的高度
        col_height = [0 for col in range(0, self.map_width)]
        # 每一列的方块数
        col_block_num = [0 for col in range(0, self.map_width)]

        # 消除行后计算各种空格数
        for row in range(0, self.map_height - 4):
            for column in range(0, self.map_width):
                if map_block[row][column] == 1:
                    col_block_num[column] = col_block_num[column] + 1
                    if row > col_height[column]:
                        col_height[column] = row
            if row < hight_max - 2 and sum(map_block[row]) < self.map_width - 2:
                space_row_2 = space_row_2 + 1
            elif row < hight_max - 2 and sum(map_block[row]) < self.map_width - 1:
                space_row_1 = space_row_1 + 1

        # 计算高度差之和
        for col in range(0, self.map_width):
            space = space + (col_height[col] - col_block_num[col])
            if col < self.map_width - 1:
                height = abs(col_height[col + 1] - col_height[col])
            else:
                height = abs(col_height[col] - col_height[col - 1])

            if height > 2:
                height_diff = height_diff + height - 2

        value = space * weight_space + \
                height_diff * weight_height_diff + \
                complete * weight_complete + \
                hight_max * weight_hight_max + \
                space_row_1 * weight_space_row_1 + \
                space_row_2 * weight_space_row_2

        return value

    # 计算此局游戏得分,不需要界面
    def cal_score(self):
        self.init_data()

        self.rotate_num, self.x_offset = self.get_x_rotate_num(self.map_block, self.cur_id)

        while True:
            if self.rotate_num > 0:
                # 旋转一次
                self.rotate()
                self.rotate_num = self.rotate_num - 1
            if self.x_offset > 0:
                # 右移一次
                self.x_offset = self.x_offset - 1
                self.move_left_right(1)
            if self.x_offset < 0:
                # 左移一次
                self.x_offset = self.x_offset + 1
                self.move_left_right(-1)

            # 继续下落
            is_bottom = self.block_move_down()
            if is_bottom:
                self.check_complete()

            if self.score > self.score_min:
                return self.score_min

            if self.gameover:
                # 游戏结束后不退出
                # sys.exit()
                return self.score

    # 计算适应度值,内部循环了数次取最差值返回
    def cal_value(self, params):
        self.score_min = 9999999
        self.weight_params = params
        scores = []
        num = 4
        for i in range(0, num):
            score = self.cal_score()
            if 1000 < score < self.score_min:
                self.score_min = score
            scores.append(min(score, self.score_min))

        return sum(scores) / num


if __name__ == '__main__':
    game = GameTetrisAuto()
    game.init_data()

    # 对参数进行优化
    # 维度
    dim = 6
    # 种群数量
    size = 20
    # 最大迭代次数
    iter_max = 1000

    range_max_list = np.array([10, 10, 10, 10, 10, 10])
    # 取值范围下界
    range_min_list = np.array([-10, -10, -10, -10, -10, -10])

    # 实例化差分进化算法类
    de_base = DE_Base(dim, size, iter_max, range_min_list, range_max_list)

    game.top = game.map_height - 10


    def fit_function(**kwargs):
        params = kwargs['params']
        if params is None:
            params = []
            game.top = int(game.map_height / 3)
        return game.cal_value(params)


    de_base.fitfunction = fit_function
    de_base.run()
    print(de_base.position_best.tolist())

    # 取出上一步的结果传回给游戏
    param_list = [8.64986860882939, 3.0577645457613123, -3.555880051448894, 1.6129266015244417, 2.4287251847128744,
                  4.67971499710595]

    game.top = game.map_height - 2
    game.weight_params = param_list
    game.run_auto()
    print(game.score)

上面注释掉的代码是使用算法求解最优解的代码,后面的param_list是我求解出来的一组解,大约能跑数千分(暴毙是肯定会暴毙的,概率问题)。
最终运行效果如下:

大家可以自己改造一下模型,求一求最优解,这里没有优化很久,抛砖引玉了。
  我的所有代码的目录如下表,如果代码import报错可以按照我的代码目录进行修改,也可以根据ide的提示自行变更。

\game\tetris\GameTetris.py
\game\tetris\GameTetrisAuto.py
\optimization_algorithm\frame\Unit.py
\optimization_algorithm\frame\Algorithm_Impl.py
\optimization_algorithm\algorithm_differential_evolution\DE_Unit.py
\optimization_algorithm\algorithm_differential_evolution\DE_Base.py
\optimization_algorithm\algorithm_differential_evolution\DE_Impl.py

五. 总结

本文使用优化算法实现了俄罗斯方块的自动化运行。
  实质上是建立了一个模型让游戏自动运行,然后使用优化算法对模型进行调优。可以看出即使是一个简单的游戏,使用优化算法来求解时也不像面对测试函数那般直接运行即可,我们会遇到不少的细节问题。
  在这次应用中,遇到了两个最关键的问题是:
  1.适应度函数不是一个幂等性函数,即使是同样的输入,输出的结果可能会有很大的差别;
  2.适应度函数的运行时间也不是固定的,越优的解所需要的运行时间也越长(假设最优解能让游戏无限制的玩下去,那么其运行时间亦是无限长的,那么我们永远也无法得到最优解)。
  面对这些问题,得明确我们的目的是什么,这里是让游戏自行长时间的运行下去。
  问题1,我在求解适应度函数是循环了数次,选取最差值作为当前输入参数的适应度值,即选取下限最高的参数。但这也在加重问题2。
  问题2,为了减少运行时间,我修改了“训练集”,让算法求解时,游戏结束条件变得更为苛刻,在正常运行时,游戏结束条件较为宽松。

此文仅供娱乐,不是什么游戏都能(适合)用优化算法求解的。

你可能感兴趣的:(优化算法应用(二)自动化俄罗斯方块)