优化算法应用(三)优化羊了个羊


  羊了个羊是前段时间很火的一款广告,其玩法简单,但是通关困难。这里,我们将广告难度降一降,确保其有通关的可能。

一. 目标描述

在玩儿的时候真的不知道这游戏是否有解,感觉就像一个迷宫一样,但是不知道起点和终点之间是否有一条通路。所以这里要确保游戏有解,同时保持游戏的难度不太低。
  由于不便使用游戏源码,这里自行实现了一个类似的游戏,用于计算和判断,游戏就叫《马了个马》吧。
实现目标有三步:

  1. 实现《马了个马》。
  2. 为游戏编写自动模式,判断游戏是否有解。
  3. 使用优化算法确保游戏有解。

二. 实现《马了个马》

1. 图片素材

图片素材没有《羊了个羊》那么多,一共12张图片。

0.jpg
1.jpg
2.jpg
3.jpg
4.jpg
5.jpg
6.jpg
7.jpg
8.jpg
9.jpg
10.jpg
11.jpg

2. 游戏的数据结构

游戏中不同层之间会相互遮挡,被遮住的图片会无法点击。
  为了方便后续计算,我将图片存储在了一个“链表树”中。每一张图都是其中的一个节点,被自己遮挡的图片为自己的子节点,遮挡自己的图片为自己的父节点。每个节点都要记录下自己全部的父节点和子节点。
  这样,就有了一个较为简单判断图片能否点击的方法:如果一张图片的父节点数量为0,那么它可以点击。让一张图片成为可点击所需的最少步数为其父节点数量之和(也包括父节点的父节点,递归计算)。


  如上图,其节点情况如下表

颜色 父节点 子节点
橙,绿
绿

从表中可知,红色图片为可点击图片,蓝色图片不可点击,变为可点击至少要1步(点击红色),橙色和绿色为不可点击图片,变为可点击至少需要2步(依次点击红色,蓝色图片)。
  该节点的代码实现如下:

class CardTreeNode:

    level = -1
    x = -1
    y = -1
    image_id = -1
    parent_list = []
    child_list = []

    def __init__(self,level,x,y,image_id):
        self.level = level
        self.x = x
        self.y = y
        self.image_id = image_id
        self.parent_list = []
        self.child_list = []

其中level,x,y表示该图片在游戏地图中的位置,parent_list为父节点的位置列表,child_list为子节点的位置列表。

3. 游戏实现

将图片放在同一文件夹下即可。
  代码如下:

import sys

import tkinter
from math import floor
import numpy as np
import pygame
import pygame.gfxdraw
from tkinter import messagebox

from game.hooorse.CardTreeNode import CardTreeNode


class GameHooorse:

    # 卡片尺寸
    card_length = 60
    # 窗口大小:平铺多少张卡片
    window_height = 12
    window_width = 8

    # 卡的种类
    type_num = 12

    # 每种卡的组数
    card_num = 1

    # 总层数
    level_num = 2

    # 各层各边卡片数
    level_card_num_1 = 6
    level_card_num_2 = 5

    # 每层卡片的左上位置
    left_1 = (window_width - level_card_num_1) / 2
    top_1 = (window_height - level_card_num_1) / 2 - 2
    left_2 = (window_width - level_card_num_2) / 2
    top_2 = (window_height - level_card_num_2) / 2 - 2

    # 卡片图
    card_tree_map = dict()

    # 卡片槽
    sorted_list = []
    # 卡片槽最大值
    max_sorted = 10

    # 卡片总数
    all_card_num = 0
    # 剩余卡片数量
    left_card = 0

    gameover = -1 # -1 未完成,0 失败,1,完成

    # 初始化数据
    def init_data(self):
        self.card_tree_map.clear()
        self.gameover = -1
        self.sorted_list = []
        self.all_card_num = 3 * self.card_num * self.type_num
        self.left_card = self.all_card_num

    # 初始化卡片
    def init_card(self):
        # 卡列表,记录所有的卡
        card_list = []
        # 将卡按顺序加入数组
        for i in range(self.type_num):
            for j in range(self.card_num):
                card_list.append(i)
                card_list.append(i)
                card_list.append(i)

        # 空白的卡数量,即不存在的卡
        left_space_num = - self.type_num * self.card_num * 3
        for i in range(0, self.level_num):
            if i % 2 == 0:
                left_space_num = left_space_num + self.level_card_num_1 * self.level_card_num_1
            else:
                left_space_num = left_space_num + self.level_card_num_2 * self.level_card_num_2

        # 在卡列表中混入不存在的卡
        for i in range(left_space_num):
            card_list.append(-1)

        # 将数组乱序
        np.random.shuffle(card_list)

        # 按顺序将乱序卡组填入地图中
        cursor = 0
        for i in range(0, self.level_num):
            if i % 2 == 0:
                level_card_num = self.level_card_num_1
            else:
                level_card_num = self.level_card_num_2

            for j in range(level_card_num):
                for k in range(level_card_num):
                    key = (i, j, k)
                    if card_list[cursor] == -1:
                        cursor = cursor + 1
                        continue
                    # 将有图片的位置保存为树
                    node = CardTreeNode(i, j, k, card_list[cursor])
                    self.card_tree_map[key] = node
                    cursor = cursor + 1
        self.cal_can_click()

    # 根据id获取图像
    def get_image_by_id(self, id):
        image_name = str(int(id)) + ".jpg"
        image = pygame.image.load(image_name).convert()
        image = pygame.transform.scale(image, (self.card_length, self.card_length))
        return image

    # 计算图片是否能被点击
    def cal_can_click(self):
        # 逆序遍历
        for i in range(self.level_num - 1, -1, -1):
            if i % 2 == 0:
                for j in range(self.level_card_num_1):
                    for k in range(self.level_card_num_1):
                        parent_key = (i, j, k)
                        if parent_key not in self.card_tree_map:
                            continue

                        for m in range(i - 1, -1, -1):
                            if m % 2 == 0:
                                child_key = (m, j, k)
                                # 不相邻层,位置相同
                                # 被挡住了不能点击
                                self.set_parent_child(parent_key, child_key)
                            else:
                                # 四个角
                                if j == 0 and k == 0:
                                    child_key = (m, j, k)
                                    self.set_parent_child(parent_key, child_key)
                                elif j == 0 and k == self.level_card_num_1 - 1:
                                    child_key = (m, 0, self.level_card_num_2 - 1)
                                    self.set_parent_child(parent_key, child_key)
                                elif j == self.level_card_num_1 - 1 and k == 0:
                                    child_key = (m, self.level_card_num_2 - 1, 0)
                                    self.set_parent_child(parent_key, child_key)
                                elif j == self.level_card_num_1 - 1 and k == self.level_card_num_1 - 1:
                                    child_key = (m, self.level_card_num_2 - 1, self.level_card_num_2 - 1)
                                    self.set_parent_child(parent_key, child_key)
                                # 四条边
                                elif j == 0:
                                    child_key = (m, 0, k)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, 0, k - 1)
                                    self.set_parent_child(parent_key, child_key)
                                elif k == 0:
                                    child_key = (m, j, 0)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, j - 1, 0)
                                    self.set_parent_child(parent_key, child_key)
                                elif j == self.level_card_num_1 - 1:
                                    child_key = (m, self.level_card_num_2 - 1, k)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, self.level_card_num_2 - 1, k - 1)
                                    self.set_parent_child(parent_key, child_key)
                                elif k == self.level_card_num_1 - 1:
                                    child_key = (m, j, self.level_card_num_2 - 1)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, j - 1, self.level_card_num_2 - 1)
                                    self.set_parent_child(parent_key, child_key)
                                else:
                                    # 内部
                                    child_key = (m, j, k)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, j - 1, k)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, j, k - 1)
                                    self.set_parent_child(parent_key, child_key)
                                    child_key = (m, j - 1, k - 1)
                                    self.set_parent_child(parent_key, child_key)

            else:
                for j in range(self.level_card_num_2):
                    for k in range(self.level_card_num_2):
                        parent_key = (i, j, k)
                        if parent_key not in self.card_tree_map:
                            continue
                        for m in range(i - 1, -1, -1):
                            if m % 2 == 0:
                                child_key = (m, j, k)
                                self.set_parent_child(parent_key, child_key)
                                child_key = (m, j + 1, k)
                                self.set_parent_child(parent_key, child_key)
                                child_key = (m, j, k + 1)
                                self.set_parent_child(parent_key, child_key)
                                child_key = (m, j + 1, k + 1)
                                self.set_parent_child(parent_key, child_key)
                            else:
                                # 被挡住了不能点击
                                child_key = (m, j, k)
                                self.set_parent_child(parent_key, child_key)

    def set_parent_child(self, parent_key, child_key):
        if parent_key not in self.card_tree_map:
            return
        if child_key not in self.card_tree_map:
            return
        self.card_tree_map[child_key].parent_list.append(parent_key)
        self.card_tree_map[parent_key].child_list.append(child_key)

    # 点击事件
    def on_click(self, pos):
        x = pos[0]
        y = pos[1]
        # # 逆序遍历
        for i in range(self.level_num - 1, -1, -1):
            if i % 2 == 0:
                # 将点击坐标转化为图片的左上角位置
                x_index = floor((x / self.card_length) - self.left_1)
                y_index = floor((y / self.card_length) - self.left_1)
                if x_index < 0 or x_index >= self.level_card_num_1:
                    continue
                if y_index < 0 or y_index >= self.level_card_num_1:
                    continue
            else:
                # 将点击坐标转化为图片的左上角位置
                x_index = floor((x / self.card_length) - self.left_2)
                y_index = floor((y / self.card_length) - self.left_2)
                if x_index < 0 or x_index >= self.level_card_num_2:
                    continue
                if y_index < 0 or y_index >= self.level_card_num_2:
                    continue

            key = (i, x_index, y_index)
            # 消除这一块
            if key in self.card_tree_map:
                if len(self.card_tree_map[key].parent_list) > 0:
                    continue
                self.left_card = self.left_card - 1  # 剩余卡数减1
                self.sorted_list.append(self.card_tree_map[key].image_id)
                self.sorted_list = sorted(self.sorted_list)

                # 将该块的child删除此parent
                for node in self.card_tree_map[key].child_list:
                    self.card_tree_map[node].parent_list.remove(key)
                self.card_tree_map.pop(key)
                self.remove_3_card()
                return

    # 3消
    def remove_3_card(self):
        cur_id = -1
        same_num = 1
        index = 0
        remove_index_list = []
        for i in self.sorted_list:
            if i == cur_id:
                same_num = same_num + 1
            else:
                cur_id = i
                same_num = 1
            if same_num == 3:
                remove_index_list.extend([index,index - 1,index - 2])
                same_num = 0

            index = index + 1

        temp_list = []

        for index in range(len(self.sorted_list)):
            if index not in remove_index_list:
                temp_list.append(self.sorted_list[index])

        self.sorted_list.clear()
        self.sorted_list.extend(temp_list)

        if len(self.sorted_list) >= self.max_sorted:
            self.gameover = 0
        else:
            if self.left_card == 0:
                self.gameover = 1

    # 绘制屏幕
    def draw_screen(self):
        # 绘制每一层的图片
        for i in range(self.level_num):
            if i % 2 == 0:
                # 绘制偶数层图片
                level_card_num = self.level_card_num_1
                left = self.left_1
                top = self.top_1
            else:
                # 绘制偶数层图片
                level_card_num = self.level_card_num_2
                left = self.left_2
                top = self.top_2

            for j in range(level_card_num):
                for k in range(level_card_num):
                    key = (i, j, k)
                    if key not in self.card_tree_map:
                        continue
                    image_id = self.card_tree_map[key].image_id
                    if image_id == -1:
                        continue
                    image = self.get_image_by_id(image_id)
                    pygame.draw.rect(image, (255, 255, 255), [0, 0, self.card_length, self.card_length], 2,
                                     border_radius=3)
                    # 如果不能点击则降低透明度
                    if len(self.card_tree_map[key].parent_list) > 0:
                        pygame.gfxdraw.box(image, [0, 0, self.card_length, self.card_length], (0, 0, 0, 150))
                    self.screen.blit(image,
                                     ((j + left) * self.card_length, (k + top) * self.card_length))
        # 绘制下方槽位
        pygame.draw.rect(self.screen, (255, 255, 255), (
            0, (self.window_height - 3) * self.card_length, self.window_width * self.card_length, 2 * self.card_length),
                         1)
        # 绘制槽位内的图
        for i in range(len(self.sorted_list)):
            image = self.get_image_by_id(self.sorted_list[i])
            pygame.draw.rect(image, (255, 255, 255), [0, 0, self.card_length, self.card_length], 2, border_radius=3)
            if i < 8:
                self.screen.blit(image, (i * self.card_length, (self.window_height - 3) * self.card_length))
            else:
                self.screen.blit(image, (i % 8 * self.card_length, (self.window_height - 2) * self.card_length))

    def run(self):
        root = tkinter.Tk()
        root.withdraw()
        pygame.init()
        self.screen = pygame.display.set_mode(
            (self.window_width * self.card_length, self.window_height * self.card_length))

        pygame.display.set_caption("马了个马")

        while True:
            self.screen.fill((110, 110, 110))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                elif event.type == pygame.MOUSEBUTTONUP:
                    self.on_click(event.pos)

            self.draw_screen()

            # 成功或失败
            if self.gameover == 0:
                messagebox.showinfo("游戏失败", "游戏失败")
                self.init_data()
                self.init_card()
            elif self.gameover == 1:
                messagebox.showinfo("游戏结束", "游戏完成")
                self.init_data()
                self.init_card()

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


if __name__ == "__main__":
    game = GameHooorse()
    # 层数
    game.level_num = 2
    # 每种卡片组数
    game.card_num = 1
    game.init_data()
    game.init_card()
    game.run()

代码运行效果如下:

三. 自动化《马了个马》

为了判断游戏是否有解,我需要电脑帮我去判断,总不能靠人工去玩儿吧。于是我根据我的游玩习惯,编写了一个自动玩儿的代码来进行游戏。注意该代码运行有解是游戏有解的非充分非必要条件,仅提供一个估算作用。

1. 规则

计算3消一类图片所需要的点击数,然后将所需点击数从小到大排列,选择点击数最少的图片进行点击。


  如上图,可点击的图片中,3消完成所需图片数量如下:



  易知,蝴蝶是所需点击数最少的图片,所以下一步会优先点击蝴蝶。
  在实际的游戏中,情况要复杂的多,对每一张图片计算所需点击数计算量也不小。为了减少计算量,我会按照以下方式来遍历:
  1. 计算父节点数量为0的图片类型所需的点击数,如果无解,则跳到2,否则返回解
  2. 计算父节点不大于1的图片类型所需的点击数,如果无解,则跳到3,否则返回解
  3. 计算父节点不大于1的图片类型所需的点击数,如果无解,则跳到4,否则返回解
  4. 。。。。
  按照这种方式,上面的游戏中,将不会计算麻雀图片所需要的点击数,因为麻雀图片每一张图都有至少一个父节点。在上图中其实影响不大,不过上图只有两层,而实际游戏中可能会有几十层,彼时将会节约巨大的计算量。
  按照上述规则,如果游戏能够结束,那么则游戏必定有解。

2. 代码实现

import sys

import tkinter
from tkinter import messagebox

import pygame

from game.hooorse.GameHooorse import GameHooorse


class GameHooorseAuto(GameHooorse):
    times = 0

    # 点击事件
    def on_click_xy(self, key):
        # 消除这一块
        if key in self.card_tree_map:
            if len(self.card_tree_map[key].parent_list) > 0:
                return
            self.left_card = self.left_card - 1  # 剩余卡数减1
            self.sorted_list.append(self.card_tree_map[key].image_id)
            self.sorted_list = sorted(self.sorted_list)

            # 将该块的child删除此parent
            for node in self.card_tree_map[key].child_list:
                self.card_tree_map[node].parent_list.remove(key)
            # 将点击的图片移除
            self.card_tree_map.pop(key)
            # 检查能否3消
            self.remove_3_card()
            return

    def run_auto(self):
        root = tkinter.Tk()
        root.withdraw()
        pygame.init()
        self.screen = pygame.display.set_mode(
            (self.window_width * self.card_length, self.window_height * self.card_length))

        pygame.display.set_caption("马了个马")

        while True:
            self.screen.fill((110, 110, 110))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                elif event.type == pygame.MOUSEBUTTONUP:
                    self.on_click(event.pos)

            if self.times >= 5:
                # 计算点击位置并点击
                pos = self.get_click_pos()
                self.on_click_xy(pos)
                self.times = 0
            else:
                self.times += 1

            self.draw_screen()

            # 成功或失败
            if self.gameover == 0:
                messagebox.showinfo("游戏失败", "游戏失败")
                self.init_data()
                self.init_card()
            elif self.gameover == 1:
                messagebox.showinfo("游戏结束", "游戏完成")
                self.init_data()
                self.init_card()

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

    # 计算此次点击的位置
    def get_click_pos(self):

        # 计算槽位中各图片的数量
        sorted_id_num = dict()
        for id in self.sorted_list:
            if id not in sorted_id_num:
                sorted_id_num[id] = 1
            else:
                sorted_id_num[id] = sorted_id_num[id] + 1

        # 将可点击的图片也按照槽中图片计算,只是数量为0
        for key in self.card_tree_map:
            image_id = self.card_tree_map[key].image_id
            if image_id not in sorted_id_num:
                sorted_id_num[image_id] = 0

        result_pos = None
        pos_list_map = dict()
        pos_num_map = dict()
        # 阈值:父类压盖数量
        threshold_num = 0
        while result_pos is None:
            # 遍历槽中的图片:下方槽中或者上方可点击的类型
            for image_id in sorted_id_num:
                # 计算点击该类图片实现3消所需的点击次数
                pos_list = self.get_click_pos_by_image_id(image_id, 3 - sorted_id_num[image_id], threshold_num)
                if pos_list is None or len(pos_list) == 0:
                    continue
                if len(pos_list) == 1:
                    result_pos = pos_list[0]
                    break
                # 将点击位置序列存入map,key为序列长度
                pos_list_map[image_id] = pos_list
                pos_num_map[image_id] = len(pos_list)

            if result_pos is None:
                if len(pos_list_map) > 0:
                    # 选择点击次数最少的类型来点击
                    image_id = sorted(pos_num_map.items(), key=lambda x: x[1], reverse=False)[0][0]
                    result_pos = pos_list_map[image_id][0]
            # 在该阈值下找不到符合条件的位置则降低条件
            threshold_num = threshold_num + 1
        return result_pos

    # 通过图片id,需要点击的数量,父类数量确定点击序列
    def get_click_pos_by_image_id(self, image_id, num, threshold_num):

        # 获取点击序列及父类数量
        pos_parent_map = dict()
        pos_parent_num = dict()
        for key in self.card_tree_map:
            node = self.card_tree_map[key]
            if node.image_id != image_id:
                continue
            if len(node.parent_list) > threshold_num:
                continue
            parent_pos_list = self.get_parent_list(key)
            pos_parent_map[key] = parent_pos_list
            pos_parent_num[key] = len(parent_pos_list)

        result_list = []
        # 按需要点击的图片数量,从小到大排序
        sorted_num = sorted(pos_parent_num.items(), key=lambda x: x[1], reverse=False)

        if num > len(sorted_num):
            return None
        # 选取点击数量最少的序列作为结果返回
        for i in range(num):
            key = sorted_num[i][0]  # 排在第一位的key
            pos_parent_map[key].reverse()
            result_list.extend(pos_parent_map[key])

        return result_list

    # 递归,获取指定位置的父节点
    def get_parent_list(self, key):
        node = self.card_tree_map[key]
        parent_list = [key]
        for parent_key in node.parent_list:
            if parent_key not in parent_list:
                parent_list.append(parent_key)
            else:
                # 将父节点放在最后
                parent_list.remove(parent_key)
                parent_list.append(parent_key)
            pp_list = self.get_parent_list(parent_key)
            for p in pp_list:
                if p not in parent_list:
                    parent_list.append(p)
                else:
                    # 将父节点放在最后
                    parent_list.remove(p)
                    parent_list.append(p)
        return parent_list


if __name__ == '__main__':
    game = GameHooorseAuto()
    game.level_num = 2
    game.card_num = 1
    game.max_sorted = 7

    # 初始化数据
    game.init_data()
    # 初始化方块
    game.init_card()
    game.run_auto()

其运行效果如下:

四. 优化《马了个马》

1.测试通关率

优化之前先看看代码通关的概率。
  每类图片10组,共10*12*3=360张图片。
  游戏层数,由于每两层的图片数为6*6+5*5=61张,故至少需要12层。
  每次游戏如果失败,游戏的完成度=消除图片数/图片总数。
  每次进行100次游戏

层数 通关率 完成度
12 0.36 0.7954
13 0.27 0.734
14 0.27 0.7116
15 0.06 0.5726
16 0.07 0.5022
17 0.07 0.5395
18 0.02 0.4365
19 0.03 0.4277
20 0.01 0.4217
21 0.02 0.3964
22 0 0.3537
23 0 0.3250
24 0 0.3499
25 0 0.3340
26 0 0.2993
27 0 0.3217
28 0 0.3094
29 0.01 0.3317
30 0 0.3299

可以看到,在图片数量为360时,若层数超过15层,通关率将直线下降,最终将会稳定在0,完成度为1/3左右。
  后面优化时将会使用26层来进行试验。

2.优化方式

显然,这次优化的适应度函数为游戏的完成度,目标就是将完成度提升到100%。
  那么如何提升游戏的完成度呢?
  我的方案是:交换两张图片位置,如果完成度提高则保留该操作。反复进行该操做,完成度到达100%时停止。


  上图中每一次循环都是一次完整的优化过程,即每一次循环都是在优化算法中经历数次迭代,最后得出结果去更新地图。每一次循环都会交换两张图片的位置,优化算法则是用来确定每一次交换哪两张图片。

3.代码实现

import copy


from game.hooorse.GameHooorseAuto import GameHooorseAuto
from optimization_algorithm.algorithm_differential_evolution.DE_Base import DE_Base
import numpy as np


class GameHooorseAutoOpt(GameHooorseAuto):
    # 卡片位置列表,优化时使用
    card_key_list = None

    # 传入卡片树,后面需要
    def set_card_tree_map(self, tree_map):
        self.card_tree_map.clear()
        for key in tree_map:
            node = copy.deepcopy(tree_map[key])
            self.card_tree_map[key] = node
        self.card_key_list = sorted(self.card_tree_map)

    def get_score(self, params):
        if params is not None and len(params) > 0:
            id1 = int(params[0])
            id2 = int(params[1])

            key1 = self.card_key_list[id1]
            key2 = self.card_key_list[id2]

            image_1 = self.card_tree_map[key1].image_id
            image_2 = self.card_tree_map[key2].image_id

            # 交换选择的两个位置的图片
            self.card_tree_map[key1].image_id = image_2
            self.card_tree_map[key2].image_id = image_1

        while True:

            if self.times >= 5:
                pos = self.get_click_pos()
                self.on_click_xy(pos)
                self.times = 0
            else:
                self.times += 1

            if self.gameover != -1:
                # 计算完成率
                return 1 - self.left_card / self.all_card_num


def opt():
    game = GameHooorseAutoOpt()
    game.level_num = 26
    game.card_num = 10
    game.max_sorted = 7

    game.init_data()
    game.init_card()

    # 复制原有的卡片树用以保存原游戏
    temp_card_tree_map = dict()
    for key in game.card_tree_map:
        node = copy.deepcopy(game.card_tree_map[key])
        temp_card_tree_map[key] = node

    # 维度
    dim = 2
    # 种群数量
    size = 5
    # 最大迭代次数
    iter_max = 10

    range_max_list = np.array([game.all_card_num - 1, game.all_card_num - 1])
    # 取值范围下界
    range_min_list = np.array([0, 0])

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

    def fit_function(**kwargs):
        game.init_data()
        game.set_card_tree_map(temp_card_tree_map)

        params = kwargs['params']
        if params is None:
            params = []
        return game.get_score(params)

    de_base.fitfunction = fit_function
    best_value = 0
    update_time = 0
    while best_value < 1:
        de_base.run()
        print(de_base.position_best.tolist())
        best_value = de_base.value_best

        id1 = int(de_base.position_best[0])
        id2 = int(de_base.position_best[1])

        key1 = game.card_key_list[id1]
        key2 = game.card_key_list[id2]
        image_1 = temp_card_tree_map[key1].image_id
        image_2 = temp_card_tree_map[key2].image_id

        # 交换选择的两个位置的图片
        temp_card_tree_map[key1].image_id = image_2
        temp_card_tree_map[key2].image_id = image_1

        update_time = update_time + 1
        print("更新次数", update_time)

    game.init_data()
    game.set_card_tree_map(temp_card_tree_map)

    # 电脑自己玩
    game.run_auto()

    # 玩家玩
    # game.run()

if __name__ == '__main__':
    opt()

运行效果如下:

4.附,差分进化代码

同上一篇
实现基本与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)

五. 总结

虽然能够保证游戏有解,但是优化的过程是不可控的,有可能需要较长的时间才能得到一个有解的地图。同时由于自动程序运行时是已知全地图,而玩家玩儿的时候无法知道被完全遮挡的图片是什么,这也会在无形之间增加了游戏的难度。
  在这一次的应用中,优化算法不止被应用了一次。每一次修改图片都会使用优化算法计算交换哪两张图片,同时为了缩减时间,每次优化算法的种群数和最大迭代次数都设置的比较小。因为仅交换两张图片就成功的概率不确定,所以这里进行了多次交换,每次交换两张,这样也更方便跳出循环进入游戏。

文件目录如下:

\game\hooorse\GameHooorse.py
\game\hooorse\GameHooorseAuto.py
\game\hooorse\GameHooorseAutoOpt.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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容