用已知队列优化对战游戏的算法


10

我正在尝试用C#.NET为称为Flowerz的游戏编写求解器。作为参考,您可以在MSN上播放它,请访问:http: //zone.msn.com/gameplayer/gameplayer.aspx?game=flowerz。我在写它只是为了好玩,而不是为了任何类型的作业或任何与工作相关的事情。因此,唯一的限制是我的计算机(具有8GB RAM的Intel i7内核)。就我而言,它不需要在其他任何地方运行。

简而言之,其规则如下:

  • 有一个装满彩色花朵的队列。它的长度是任意的
    • 队列不受影响
    • 队列在级别开始时生成
  • 花有一种或两种颜色。
    • 如果有两种颜色,则有外部颜色和内部颜色。在两种颜色的情况下,外部颜色用于匹配。
    • 如果匹配,则外部颜色消失,花朵现在是与内部花朵颜色相同的单色花朵
  • 游戏的目标是创建三个(或更多)相同颜色的比赛
    • 当单色的花朵是火柴的一部分时,它会从运动场上移开,从而创建一个空白空间
    • 您可以将单色花朵与双色花朵的外部颜色进行匹配。在这种情况下,单色花消失,两色花的外部颜色消失而内部颜色保留
  • 当队列为空剩余至少一个空白处时,您将赢得回合
  • 级联匹配是可能的。级联是指三朵(或更多)外花消失,而它们的内在颜色又形成了三朵(或更多花)链。
  • 比赛场地始终是7x7
  • 田野上的一些空间被岩石覆盖
    • 你不能在岩石上放花
  • 队列还可以包含一个小铲,您可以使用该小铲将任何放置的花朵移至空闲空间
    • 您必须使用铁锹,但实际上不必移动花:将花从原位放回原处是完全合法的
  • 队列中还可以包含彩色蝴蝶。当您在花朵上使用这只蝴蝶时,花朵会得到蝴蝶的颜色
    • 将蝴蝶应用于具有两种颜色的花朵,导致花朵仅获得一种颜色,即蝴蝶的颜色
    • 您可以将蝴蝶浪费在已经有这种颜色的空白处或花朵上
  • 清除场地并不能赢得比赛

求解器的目标很简单:找到一种清空队列的方法,在运动场上保留尽可能多的剩余空间。基本上,人工智能为我玩游戏。求解器的输出是包含找到的移动的列表。我对得分不感兴趣,但对生存时间越长越感兴趣,因此我对留下尽可能多的空地的动作感兴趣。

不用说,队列越大,搜索空间就会迅速增长,因此就不会有暴力行为了。如果我没记错的话,队列从15开始,每两到三个级别以5增长。而且,当然,将第一朵花放在(0,0)上并将第二朵花放在(0,1)与将第一朵花放在(1,0)和第二朵花(0,0)上是不同的,尤其是当早先的一轮花已经充满了田野。这样简单的决定可能会有所不同。

我的问题如下:

  • 这是什么问题?(请考虑旅行推销员,背包或其他组合问题)。知道这一点可以使我的Google-fu更好。
  • 哪种算法可以快速给我带来良好的效果?

关于后者:最初,我尝试编写自己的启发式算法(基本上:如果我知道队列,该如何解决?),但这会导致很多边缘情况和得分匹配,而我可能会错过这种情况。

我当时在考虑使用遗传算法(因为我至少知道如何使用它...),但是在确定板的二进制表示时遇到了一些问题。然后是交叉问题,但是可以使用有序交叉算子或类似类型的操作来解决。

我的猜测是,求解器必须始终知道电路板配置和它要清空的队列。

我知道其他一些启发式算法,例如神经网络和模糊逻辑系统,但是我缺乏经验,无法知道哪种算法最适用,或者是否还有其他算法更适合手头的任务。


我曾经做过一次计算,发现我正在研究的某些复杂游戏的搜索空间为32Gb。当时(我有一个20Mb的磁盘驱动器)本来是不可行的,但是如今,对于某些计算机而言,在RAM中几乎是可以实现的。
乔纳森

匹配时只有一种颜色的花朵会完全消失吗?两种颜色的花朵能否使其外层与一种颜色的花朵的单色相匹配?我认为这两种情况都是这样,但问题描述中从未明确指出这些……
史蒂文·斯塔德尼克

@StevenStadnicki谢谢!我已将该信息添加到原始问题中。
user849924

1
顺便提一句,这个问题的“布尔值”版本(是否有某种方法将鲜花排在队列中,以使电路板最后完全腾空?)的绝大多数情况是NP完全的;它与NP完全的Clickomania问题(erikdemaine.org/clickomania)有着明显的相似性,而且这个问题并不比NP难,因为给定一个声称的解决方案(多项式长度),只需运行模拟就可以轻松地进行验证。这意味着优化问题可能在FP ^ NP中。
史蒂文·斯塔德尼基

Answers:


9

乍一看,在我看来这似乎是一个单一代理商的搜索问题。即:您有一个代理(AI“玩家”)。有一个游戏状态代表游戏板和队列的状态,并且您有一个后继功能,可以从给定状态生成新状态。

还有一个目标标准,可以告诉您状态何时为“已解决”状态。和路径开销 -推进到一个给定的状态(总是“1招”,在这种情况下)的成本。

这种原型难题是15难题。解决此问题的典型方法是使用知情搜索 -例如,经典启发式搜索A *及其变体。


但是,这种乍看之下的方法存在问题。诸如A *之类的算法旨在为您提供最短的目标路径(例如:最少的移动次数)。在您的情况下,移动的次数始终是固定的-没有最短的路径-因此启发式搜索只会为您提供完成游戏路径。

您想要的是一系列动作,这些动作可以为您提供最佳的完整游戏状态。

因此,您必须做的是解决问题。代替游戏板为“状态”,移动顺序变为“状态”。(即:将项目放入队列中的位置“ D2,A5,C7,B3,A3,...”)

这意味着我们并不真正在乎这些状态是如何产生的。电路板本身是偶然的,仅需要评估给定状态的质量即可。

这就把问题变成了优化问题,可以用局部搜索算法解决(基本上意味着在给定状态周围创建状态,并选择最佳状态,而无需关心状态之间的路径)。

的原型之谜一种是八个皇后问题

在此类问题中,您正在搜索状态空间以找到一个好的解决方案,其中“好的”是通过目标函数(也称为评估函数,或者对于遗传算法,是适应性函数)进行评估

对于您的问题,对于到达故障状态(其中N是队列的长度)之前队列中已用完的项目数,目标函数可能返回0到N之间的值。并且,否则为N + M的值,其中M是队列为空后板上剩余的空白数。这样-值越高,解决方案“客观上越好”。

(在这一点上,值得注意的是,您应该从运行游戏的代码中优化废话-将状态变成成品板,以用于目标功能。)


至于本地搜索算法的示例:基本模式是爬山搜索,它采用给定状态,对其进行变异,然后移至下一个状态,从而获得更好的结果。

显然,这可能会卡在局部最大值中(等等)。这种形式称为贪婪的本地搜索。有很多变体可以处理此问题和其他问题(Wikipedia已涵盖)。其中一些(例如:局部波束搜索)可同时跟踪多个状态。

遗传算法Wikipedia)就是其中一种。遗传算法的基本步骤是:

  1. 确定将状态转换为某种类型的字符串的某种方法。在您的情况下,这可能是一串从1到49的队列长度数字(代表7x7板上的所有可能放置,每个位置可能存储1个字节)。(对于移动的每个阶段,您的“铲子”都可以由两个后续队列条目表示。)
  2. 随机选择一个繁殖种群,使适应性更好的州获得更高的概率。繁殖种群的大小应与原始种群相同-您可以多次从原始种群中选择州。
  3. 配对繁殖种群中的州(第一和第二,第三和第四,依此类推)
  4. 为每对随机选择交叉点(字符串中的位置)。
  5. 通过交换交叉点之后的字符串部分,为每对创建两个后代。
  6. 随机变异每个后代状态。例如:随机选择将字符串中的随机位置更改为随机值。
  7. 对新种群重复该过程,直到种群收敛于一个或多个解(或在给定数量的世代之后,或者找到足够好的解)。

遗传算法求解感觉它可能适合你的问题-一些调整。我看到的最大困难是,使用上面的字符串表示法,您将发现切换前半部状态非常不同的状态的后半部可能会导致“死”状态(由于两个半部之间的移动相互冲突,导致健身得分较低)。

也许有可能克服这个问题。我想到的一个想法是,前半部分相似的州更有可能成为配对。这可以像对州的繁殖种群进行排序之前一样简单。随着世代数的增加,它也可能有助于逐渐将分频器的可能位置从字符串的开头移动到结尾。

也有可能提出一个状态内的运动表示,该状态对遇到“方格满”故障状态更有抵抗力(也许甚至是完全免疫)。也许将移动表示为相对于先前移动的相对坐标。或通过移动选择最接近给定位置的空白区域。

像这样的所有非平凡的AI问题一样,这将需要进行一些重大修改。

而且,正如我之前提到的,另一个主要挑战是简单地优化目标函数。加快速度将使您可以搜索大量空间,并为队列较长的游戏搜索解决方案。


对于这个答案,特别是为了正确使用所有术语,我不得不从我的大学AI教科书中找到,该教科书是Russell和Norvig撰写的“人工智能:一种现代方法”。不知道它是否“好”(我没有其他AI文本可与之比较),但这还不错。至少它很大;)


我也发现了跨界车的问题:一个孩子放置的物品很可能比队列中可用的物品多(TSP缺少GA的种类:他可能会在一次旅行后访问城市两次或更多次(或根本没有!)。 –也许有序的交叉(permutationcity.co.uk/projects/mutants/tsp.html)可以工作,这在执行状态移动序列时特别适用
user849924 2013年

不确定是否很正确-在我看来,失败状态是将棋子放置在已经占据的位置上(因此较早结束游戏,导致健身得分较低)。因此,队列长度与遗传字符串的长度匹配-它永远不会是错误的长度。仍然-您可能会想到交换和订购的想法。如果给定的顺序产生了一个完整的游戏,而您交换了两个动作,那么我想比起简单地随机设置一个(或两个?)移动的位置,突变状态也成为完整的游戏的机会要大得多。 。
安德鲁·罗素

失败状态是当您没有其他放置移动的选项时,即当您用完空白空间并且该移动没有匹配发生时。与您所说的类似:您必须将其放置在已经占用的位置上(但是只有在没有更多可用的开始位置时才如此)。我发布的分频器可能很有趣。染色体A的项目位于A1,B1,...,G1,A2,B2和C2上,而染色体B则位于G7 ... A7,G6,F6和E6上。从A中选择一些随机数并保留其索引。从B中选择A的补语,并保留其索引并合并为一个孩子。
user849924

交叉的“问题”是允许在同一位置进行多次移动。但这应该可以很容易地通过类似于Stefan K解决方案中的SimulateAutomaticChanges的方法来解决:将子项的移动集/状态应用于运动场的基本状态(简单地逐个应用所有移动),如果接受状态(空队列) )无法实现(因为您必须在有人居住的地方放一朵花),那么孩子是无效的,我们需要再次繁殖。这是您的故障情况弹出的地方。我现在得到那个了,呵呵。:D
user849924

我接受这个答案,有两个原因。首先:您给我一个想法,我需要让GA来解决这个问题。第二:你是第一。; p
user849924

2

分类

答案并不容易。博弈论对游戏进行了一些分类,但是对于该博弈而言,似乎没有与特定理论明确的1:1匹配。这是组合问题的一种特殊形式。

它不是旅行推销员,而是要决定一个订单,在该订单中您要以一定的成本访问“节点”,以便从上一个节点到达下一个节点。您无法对队列进行重新排序,也不必使用地图上的所有字段。

背包不匹配,因为将某些项目放入“背包”时某些字段变为空。因此,这可能是其中的某种扩展形式,但是由于这个原因,最有可能算法将不适用。

维基百科在此处提供了有关分类的一些提示:http : //en.wikipedia.org/wiki/Game_theory#Types_of_games

我会将其归类为“离散时间最优控制问题”(http://en.wikipedia.org/wiki/Optimal_control),但我认为这不会对您有所帮助。

演算法

如果您真的知道完整的队列,则可以应用树搜索算法。正如您所说,问题的复杂性随着队列长度的增长而非常快。我建议使用诸如“深度优先搜索(DFS)”之类的算法,该算法不需要太多内存。由于分数对您而言并不重要,因此您可以在找到第一个解决方案之后就停下来。要确定首先要搜索的子分支,您应该应用启发式排序。这意味着您应该编写一个评估函数(例如:空字段的数量;此字段越复杂越好),该函数给出一个分数来比较哪个下一步最有前途。

然后,您只需要以下部分:

  1. 游戏状态的模型,用于存储游戏的所有信息(例如,棋盘状态/地图,队列,移动编号/队列中的位置)
  2. 移动生成器,可为您提供给定游戏状态的所有有效移动
  3. “执行移动”和“撤消移动”功能;应用/撤消给定(有效)的游戏状态。而“执行移动”功能应为“撤消”功能存储一些“撤消信息”。复制游戏状态并在每次迭代中对其进行修改都会大大降低搜索速度!尝试至少将状态存储在堆栈上(=局部变量,不使用“ new”进行动态分配)。
  4. 评估功能,可为每个游戏状态提供可比分数
  5. 搜索功能

这是深度优先搜索的不完整参考实现:

public class Item
{
    // TODO... represents queue items (FLOWER, SHOVEL, BUTTERFLY)
}

public class Field
{
    // TODO... represents field on the board (EMPTY or FLOWER)
}

public class Modification {
    int x, y;
    Field originalValue, newValue;

    public Modification(int x, int y, Field originalValue, newValue) {
        this.x = x;
        this.y = y;
        this.originalValue = originalValue;
        this.newValue = newValue;
    }

    public void Do(GameState state) {
        state.board[x,y] = newValue;
    }

    public void Undo(GameState state) {
        state.board[x,y] = originalValue;
    }
}

class Move : ICompareable {

    // score; from evaluation function
    public int score; 

    // List of modifications to do/undo to execute the move or to undo it
    Modification[] modifications;

    // Information for later knowing, what "control" action has been chosen
    public int x, y;   // target field chosen
    public int x2, y2; // secondary target field chosen (e.g. if moving a field)


    public Move(GameState state, Modification[] modifications, int score, int x, int y, int x2 = -1, int y2 = -1) {
        this.modifications = modifications;
        this.score = score;
        this.x = x;
        this.y = y;
        this.x2 = x2;
        this.y2 = y2;
    }

    public int CompareTo(Move other)
    {
        return other.score - this.score; // less than 0, if "this" precededs "other"...
    }

    public virtual void Do(GameState state)
    {
        foreach(Modification m in modifications) m.Do(state);
        state.queueindex++;
    }

    public virtual void Undo(GameState state)
    {
        --state.queueindex;
        for (int i = m.length - 1; i >= 0; --i) m.Undo(state); // undo modification in reversed order
    }
}

class GameState {
    public Item[] queue;
    public Field[][] board;
    public int queueindex;

    public GameState(Field[][] board, Item[] queue) {
        this.board = board;
        this.queue = queue;
        this.queueindex = 0;
    }

    private int Evaluate()
    {
        int value = 0;
        // TODO: Calculate some reasonable value for the game state...

        return value;
    }

    private List<Modification> SimulateAutomaticChanges(ref int score) {
        List<Modification> modifications = new List<Modification>();
        // TODO: estimate all "remove" flowers or recoler them according to game rules 
        // and store all changes into modifications...
        if (modifications.Count() > 0) {
            foreach(Modification modification in modifications) modification.Do(this);

            // Recursively call this function, for cases of chain reactions...
            List<Modification> moreModifications = SimulateAutomaticChanges();

            foreach(Modification modification in modifications) modification.Undo(this);

            // Add recursively generated moves...
            modifications.AddRange(moreModifications);
        } else {
            score = Evaluate();
        }

        return modifications;
    }

    // Helper function for move generator...
    private void MoveListAdd(List<Move> movelist, List<Modifications> modifications, int x, int y, int x2 = -1, int y2 = -1) {
        foreach(Modification modification in modifications) modification.Do(this);

        int score;
        List<Modification> autoChanges = SimulateAutomaticChanges(score);

        foreach(Modification modification in modifications) modification.Undo(this);

        modifications.AddRange(autoChanges);

        movelist.Add(new Move(this, modifications, score, x, y, x2, y2));
    }


    private List<Move> getValidMoves() {
        List<Move> movelist = new List<Move>();
        Item nextItem = queue[queueindex];
        const int MAX = board.length * board[0].length + 2;

        if (nextItem.ItemType == Item.SHOVEL)
        {

            for (int x = 0; x < board.length; ++x)
            {
                for (int y = 0; y < board[x].length; ++y)
                {
                    // TODO: Check if valid, else "continue;"

                    for (int x2 = 0; x2 < board.length; ++x2)
                    {
                        for(int y2 = 0; y2 < board[x].length; ++y2) {
                            List<Modifications> modifications = new List<Modifications>();

                            Item fromItem = board[x][y];
                            Item toItem = board[x2][y2];
                            modifications.Add(new Modification(x, y, fromItem, Item.NONE));
                            modifications.Add(new Modification(x2, y2, toItem, fromItem));

                            MoveListAdd(movelist, modifications, x, y, x2, y2);
                        }
                    }
                }
            }

        } else {

            for (int x = 0; x < board.length; ++x)
            {
                for (int y = 0; y < board[x].length; ++y)
                {
                    // TODO: check if nextItem may be applied here... if not "continue;"

                    List<Modifications> modifications = new List<Modifications>();
                    if (nextItem.ItemType == Item.FLOWER) {
                        // TODO: generate modifications for putting flower at x,y
                    } else {
                        // TODO: generate modifications for putting butterfly "nextItem" at x,y
                    }

                    MoveListAdd(movelist, modifications, x, y);
                }
            }
        }

        // Sort movelist...
        movelist.Sort();

        return movelist;
    }


    public List<Move> Search()
    {
        List<Move> validmoves = getValidMoves();

        foreach(Move move in validmoves) {
            move.Do(this);
            List<Move> solution = Search();
            if (solution != null)
            {
                solution.Prepend(move);
                return solution;
            }
            move.Undo(this);
        }

        // return "null" as no solution was found in this branch...
        // this will also happen if validmoves == empty (e.g. lost game)
        return null;
    }
}

该代码未经验证可工作,也不可编译或完整。但是它应该给您一个想法如何做。最重要的工作是评估功能。它越复杂,算法将在以后尝试(必须撤消)错误的“重试”。这极大地降低了复杂性。

如果这太慢,您还可以尝试将某些两人游戏方法用作HashTables。为此,您必须为您评估的每个游戏状态计算(迭代)哈希键,并标记不会导致解决方案的状态。例如,每次Search()方法返回“ null”之前,必须创建一个HashTable条目,并且在输入Search()时,您将检查到目前为止是否已经达到该状态且没有肯定结果,如果返回,则返回“ null”,而没有进一步的调查。为此,您将需要一个巨大的哈希表,并且必须接受“哈希冲突”,这可能会导致您可能找不到现有的解决方案,但是如果您的哈希函数足够好并且您的表是足够大(存在可计算风险的风险)。

我认为没有其他算法可以更有效地解决此问题(如您所述),前提是您的评估功能最佳。


是的,我知道完整的队列。评估功能的实现是否还会考虑有效但潜在的不良位置?如果在田地上已经有相似的颜色,将其放置在另一种颜色的花朵旁边,这可能是一个坏举动吗?还是因为空间不足而在完全不同的比赛块之间放一朵花?
user849924 2013年

这个答案给了我关于模型以及如何使用游戏规则的想法,所以我会投票赞成。感谢您的输入!
user849924 2013年

@ user849924:是的,评估函数当然必须为此计算评估“值”。当前游戏状态越差(接近丢失),返回的评估值就越差。最简单的评估是返回空字段的数量。您可以通过为相近颜色的花朵旁边的每朵花朵添加0.1来改进此效果。要验证您的功能,请选择一些随机的游戏状态,计算它们的值并进行比较。如果您认为状态A优于状态B,则A的得分应优于B的得分
。– SDwarfs
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.