優化算法應用(二)自動化俄羅斯方塊

這是好多年前做過的一個東西,現在用優化算法來實現一下。

一. 目標描述

俄羅斯方塊大家應該都玩過。
這里的目標是設計一個模型讓電腦計算判斷一個方塊到哪個位置最好,讓電腦自己控制方塊的平移旋轉,并使游戲能夠長時間運行下去。

二. 模型設計

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,為了減少運行時間,我修改了“訓練集”,讓算法求解時,游戲結束條件變得更為苛刻,在正常運行時,游戲結束條件較為寬松。

此文僅供娛樂,不是什么游戲都能(適合)用優化算法求解的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容