Алгоритм оптимізації гри в матчі з відомою чергою


10

Я намагаюся написати розв’язувач у C # .NET для гри, відомої як Flowerz. Для ознайомлення, ви можете відтворити його на MSN тут: http://zone.msn.com/gameplayer/gameplayer.aspx?game=flowerz . Я пишу це для розваги, а не для будь-якого типу завдання чи будь-якої роботи, пов'язаної. Через це єдиним обмеженням є мій комп'ютер (ядро Intel i7, з 8 ГБ оперативної пам’яті). Щодо мене не потрібно бігати більше ніде.

Коротше кажучи, правила такі:

  • Там черга заповнена кольоровими квітами. Його довжина довільна
    • Не можна впливати на чергу
    • Черга генерується на початку рівня
  • Квітки мають один або два кольори.
    • Якщо є два кольори, то є зовнішній і внутрішній колір. У випадку двох кольорів зовнішній колір використовується для відповідності.
    • Якщо є сірник, то зовнішній колір зникає, і квітка тепер є одноколірною квіткою такого ж кольору, як внутрішня квітка
  • Мета гри - створити матчі з трьох (або більше) одного кольору
    • Коли квітка одного кольору є частиною сірника, її видаляють з ігрового поля, створюючи порожній простір
    • Можна зіставити одноколірну квітку проти зовнішнього кольору двоколірної квітки. У цьому випадку одноколірна квітка зникає, зовнішній колір двоколірної квітки зникає, а внутрішній колір залишається
  • Ви виграєте раунд, коли черга порожня, і залишилось хоча б одне порожнє місце
  • Можливі каскадні матчі. Каскад - це коли три (або більше) зовнішніх квіток зникають, і коли їх внутрішні кольори утворюють інший ланцюжок з 3 (або більше квіток).
  • Ігрове поле завжди 7x7
  • Деякі простори на полі вкриті скелями
    • Ви не можете розмістити квіти на скелях
  • Черга також може містити лопату, яку можна використовувати для переміщення будь-якої розміщеної квітки на незайнятий простір
    • Ви повинні використовувати лопату, але насправді не потрібно переміщати квітку: цілком законно розмістити її прямо там, де вона прийшла
  • У черзі також може бути кольоровий метелик. Коли ви використовуєте цього метелика на квітці, то квітка набуває кольору метелика
    • Застосування метелика до квітки з двома кольорами призводить до того, що квітка набуває лише одного кольору, а саме кольору метелика
    • Можна пустити метелика на порожній простір або на квітку, яка вже має цей колір
  • Очищення поля не виграє гру

Мета вирішувача проста: знайти спосіб спорожнити чергу з якомога більше пробілів на ігровому полі. В основному, AI грає гру для мене. Вихід solver - це список із знайденими рухами. Мене не цікавить оцінка, але в тому, щоб вижити якомога довше, тому мене цікавлять кроки, які залишають якомога більше відкритих просторів.

Зайве говорити, що простір пошуку швидко зростає, чим більша кількість черги стає, тому груба сила не підлягає. Черга починається з 15, і зростає з 5 кожні два-три рівні, якщо я добре пам’ятаю. І, звичайно, розміщення першої квітки на (0,0), а другу на (0,1) відрізняється від розміщення першого на (1,0), а другого на (0,0), особливо коли поле вже заселене квітами з більш раннього раунду. Таке просте рішення може змінити його чи ні.

У мене такі питання:

  • Що це за проблема? (подумайте, що продавець подорожей, рюкзак чи якась інша комбінаторна проблема). Знаючи це, це може зробити мій Google-фу кращим моментом.
  • Який алгоритм може швидко дати мені хороші результати?

Щодо останнього: Спочатку я спробував написати власний евристичний алгоритм (в основному: як би я його вирішив, якби я знав чергу?), Але це призводить до безлічі крайових випадків та підрахунку балів, які я можу пропустити.

Я думав про використання генетичного алгоритму (тому що я, принаймні, знаю, як це використовувати ...), але у мене виникають певні проблеми з вирішенням питання щодо бінарного представлення дошки. Тоді виникає проблема кросовера, але це можна вирішити за допомогою впорядкованого оператора кросовера або подібного типу операцій.

Я здогадуюсь, що розв’язувач повинен завжди знати конфігурацію плати та чергу, яку вона намагається спорожнити.

Я знаю ще декілька інших евристичних алгоритмів, таких як нейронні мережі та нечіткі логічні системи, але мені бракує досвіду, щоб знати, який із них найкраще застосувати, чи є інші, які краще підходять для виконання задачі.


Я колись працював над тим, що пошуковий простір якоїсь складної гри, над якою я працював, буде 32 Гбіт. У той час (у мене був накопичувач на 20 Мб диска), це було б нездійсненно, але в наші дні це стосується лише оперативної пам'яті для деяких комп'ютерів.
Джонатан

Чи повністю зникають квіти лише одного кольору при зіставленні? І чи можуть квіти з двома кольорами відповідати своєму зовнішньому шару проти єдиного кольору одноколірної квітки? Я припускаю так в обох пунктах, але вони ніколи прямо не описані в описі проблеми ...
Стівен Стадницький

@StevenStadnicki Дякую! Я додав цю інформацію до початкового питання.
користувач849924

1
Як невелика примітка, до речі, надзвичайно ймовірно, що "булева" версія цієї проблеми (чи є якийсь спосіб розташування квітів у черзі, щоб залишити дошку повністю порожньою наприкінці?) Не є повною; вона має очевидну схожість із проблемою Clickomania ( erikdemaine.org/clickomania ), яка не є повною, і ця проблема не є більш важкою, ніж NP, оскільки з урахуванням розробленого рішення (довжини полінома) це легко перевірити, просто запустивши моделювання. Це означає, що проблема оптимізації, ймовірно, у FP ^ NP.
Стівен Стадницький

Відповіді:


9

На перший погляд , мені здається, це єдина проблема пошуку агента . Тобто: у вас є один агент (AI «гравець»). Існує ігровий стан, який представляє стан ігрової дошки та черги, і у вас є наступна функція, яка може генерувати нові стани з заданого стану.

Також є критерії цілі, які вказують, коли держава є "вирішеною" державою. А вартість шляху - вартість просування до заданого стану (у цьому випадку завжди "1 хід").

Однією з прототипних головоломок такого роду є 15 Puzzle . А типовий спосіб її вирішення - за допомогою усвідомленого пошуку - наприклад, класичний евристичний пошук A * та його варіанти.


Однак з цим підходом на перший погляд є проблема. Алгоритми типу A * покликані дати вам найкоротший шлях до цілі (наприклад: найменша кількість рухів). У вашому випадку, кількість ходів завжди фіксоване - немає найкоротшого шляху - так евристичний пошук буде просто дати вам на шляху до більш завершеною грі.

Те, що ви хочете, - це послідовність рухів, яка дає вам найкраще завершений ігровий стан.

Тож, що вам потрібно зробити, це трохи перетворити проблему. Замість того, щоб ігрова дошка була "станом", послідовність рухів стає "станом". (Тобто: розмістіть елементи в черзі в позиціях "D2, A5, C7, B3, A3, ...")

Це означає, що нас не дуже цікавить, як формуються ці держави. Сама плата є випадковою, потрібна лише для оцінки якості заданого стану.

Це перетворює проблему на оптимізаційну проблему , яку можна вирішити за допомогою алгоритму локального пошуку (що в основному означає створення станів навколо даного стану та вибір найкращого стану, не піклуючись про шлях між станами.)

Прототиповою головоломкою такого роду є вісімка королеви .

У цьому класі проблем ви шукаєте простір станів, щоб знайти гарне рішення, де "хороший" оцінюється об'єктивною функцією (також називається функцією оцінювання або, за генетичними алгоритмами, функцією фітнесу ).

Для вашої проблеми цільова функція може повернути значення між 0 і N для кількості елементів у черзі, які були використані до досягнення стану відмови (де N - довжина черги). Інакше значення N + M, де M - кількість порожніх пробілів, залишених на дошці після порожньої черги. Як таке - чим вище значення, тим "об'єктивно краще" рішення.

(Варто зазначити, що в цей момент ви повинні оптимізувати лайно з коду, який запускає гру - що перетворює стан у готову дошку, яку можна використовувати для цільової функції.)


Що стосується прикладів алгоритмів локального пошуку : Основна закономірність - це пошук на схилі, що приймає заданий стан, мутує його і рухається до наступного стану, що дає кращий результат.

Очевидно, що це може застрягти в локальних максимумах (тощо). У цій формі це називається жадібним місцевим пошуком . Існує купа варіантів для вирішення цього та інших питань ( Wikipedia ви охоплювали ). Деякі з яких (наприклад, локальний пошук променя ) відслідковують одночасно кілька станів.

Однією з цих змін є генетичний алгоритм ( Вікіпедія ). Основними кроками генетичного алгоритму є:

  1. Визначте якийсь спосіб перетворення стану в рядок якогось типу. У вашому випадку це може бути рядок цифр довжиною черги від 1 до 49 (що представляє всі можливі місця розташування на дошці 7x7, ймовірно, що зберігається по 1 байті кожна). (Ваш фрагмент "лопати" може бути представлений двома наступними записами черги для кожної фази переміщення.)
  2. Випадково відбирають племінну популяцію, даючи більшу ймовірність станам, які мають кращу придатність . Розмножена популяція повинна бути такої ж величини, як і вихідна популяція - ви можете обирати штати з вихідної популяції кілька разів.
  3. Парні стани в племінній популяції (перший йде з другим, третій йде з четвертим тощо)
  4. Випадково виберіть кроссовер для кожної пари (позиція в рядку).
  5. Створіть по два нащадки для кожної пари, помінявши частину струни після точки кросовер.
  6. Довільно мутуйте кожне із станів потомства. Наприклад: довільно вибрати, щоб змінити випадкову позицію в рядку на випадкове значення.
  7. Повторюйте процес з новою сукупністю, поки населення не сходиться на одному чи декількох рішеннях (або після заданої кількості поколінь або не знайдеться достатньо хорошого рішення).

Рішення генетичного алгоритму вважає, що воно може відповідати вашій проблемі - з певним коригуванням. Найбільша складність, яку я бачу, полягає в тому, що, виходячи з представленого вище рядка, ви виявите, що перемикання хвостових половинок станів з дуже різними передніми половинками може призвести до "мертвих" станів (через суперечливі переміщення між двома половинами, такий результат в низький бал фітнесу).

Можливо, можна подолати цю проблему. Однією з думок, яка спадає на думку, є більша ймовірність того, що держави з подібними передніми половинами стануть розмножувальними парами. Це може бути таким же простим, як сортування племінної популяції штатів, перш ніж їх поєднати. Це також може допомогти поступово переміщувати ймовірне положення кросовера від початку до кінця струни, оскільки число генерації збільшується.

Можливо, також можна скласти уявлення про рухи в стані, який є більш стійким (можливо, навіть цілком захищеним) до настання стану відмови "квадрат повний". Можливо, представляє рухи як відносні координати від попереднього ходу. Або рухаючись, виберіть найближче порожнє місце до заданої позиції.

Як і у всіх подібних нетривіальних проблемах зі ШІ, це потребує значного повороту.

І, як я вже згадував, інша основна проблема - це просто оптимізувати вашу цільову функцію. Швидше зробити це дозволить шукати велику кількість простору та шукати рішення для ігор з довшими чергами.


Для цієї відповіді, зокрема, щоб зрозуміти всю термінологію, мені довелося розкопати підручник з ІІІ університету «Штучний інтелект: сучасний підхід» Рассела та Норвіга. Не впевнений, що це "добре" (у мене немає інших текстів AI, щоб порівняти це), але це не погано. Принаймні, вона досить велика;)


Я визначив цю проблему і з кросовером: цілком можливо, що у дитини розміщено більше предметів, ніж доступних у черзі (такий вид відсутності GA для TSP: він може відвідувати міста двічі або більше (або зовсім не!) Після Можливо, впорядкований кросовер ( permutationcity.co.uk/projects/mutants/tsp.html ) міг би працювати. Це особливо застосовано, коли ви здійснюєте послідовність переходів держави
user849924

Не впевнений, що це цілком правильно - на мою думку, стан невдач полягає в тому, що шматок розміщується у вже зайнятій позиції (таким чином закінчуючи цю гру рано, в результаті чого низький бал у фітнесі). Тож довжина черги відповідає довжині генетичної нитки - вона ніколи не буває неправильної довжини. І все-таки - ви можете щось наштовхнути на думку про заміну та замовлення. Якщо заданий порядок призводить до закінченої гри, і ви поміняєте два рухи, я думаю, що є набагато більший шанс, що мутований стан також буде закінченою грою, ніж якби ви просто встановили одну (або дві?) Позиції переміщення випадковим чином .
Ендрю Рассел

Стан відмови - це те, коли у вас немає більше варіантів розміщення рухів, тобто коли у вас немає порожніх пробілів і з цим переміщенням не відбувається збігів. Подібно до того, що ви говорите: ви повинні розмістити його на вже зайнятій посаді (але це справедливо лише тоді, коли більше немає місця для початку). Кросовер, який я розмістив, може бути цікавим. У хромосомі А є елементи, розміщені на A1, B1, ..., G1, A2, B2 та C2, а хромосому B на G7 ... A7, G6, F6 та E6. Виберіть декілька randoms від A і збережіть їх індекс. Виберіть доповнення A від B і збережіть їх індекс та злиття для дитини.
користувач849924

"Проблема" цього кросовера полягає в тому, що дозволено кілька рухів на одному місці. Але це має бути легко вирішуваним чимось подібним до SimulateAutomaticChanges з рішення Стефана К: застосуйте набір переміщення / стан дитини до базового стану (просто застосуйте всі рухи, по одному) ігрового поля і якщо стан прийняття (порожня черга) ) неможливо досягти (тому що вам потрібно помістити квітку на зайняте місце), тоді дитина недійсна, і нам потрібно буде розводитись знову. Ось де спливає ваш стан відмови. Я зараз розумію, хе. : D
користувач849924

Я приймаю це як відповідь з двох причин. По-перше: ти дав мені ідею, що мені потрібно, щоб GA працював над цією проблемою. Друге: ти був першим. ; p
користувач849924

2

Категоризація

Відповідь непроста. Теорія ігор має деякі класифікації ігор, але, схоже, немає чіткого відповідності 1: 1 для цієї гри зі спеціальною теорією. Це особлива форма комбінаторної проблеми.

Це не мандрівний продавець, який вирішив би замовити, в якому ви відвідуєте "вузли" з деякою вартістю, щоб дістатись до наступного вузла з останнього. Ви не можете впорядкувати чергу, а також не потрібно використовувати всі поля на карті.

Рюкзак не відповідає, оскільки деякі поля стають порожніми під час розміщення деяких елементів у "рюкзак". Тож це може бути якась розширена форма цього, але, швидше за все, алгоритми не будуть застосовні через це.

Вікіпедія дає деякі підказки щодо категоризації тут: http://en.wikipedia.org/wiki/Game_theory#Types_of_games

Я б класифікував це як "дискретно оптимальну проблему управління" ( http://en.wikipedia.org/wiki/Optimal_control ), але я не думаю, що це вам допоможе.

Алгоритми

Якщо ви справді знаєте повну чергу, ви можете застосувати алгоритми пошуку дерев. Як ви вже говорили, складність проблеми зростає дуже швидко з довжиною черги. Я пропоную використовувати такий алгоритм на кшталт "Глибинний перший пошук (DFS)", який не потребує багато пам'яті. Оскільки оцінка для вас не має значення, ви можете просто зупинитися, знайшовши перше рішення. Щоб вирішити, яку підгалузь шукати спочатку, слід застосувати евристику для замовлення. Це означає, що ви повинні написати функцію оцінювання (наприклад: кількість порожніх полів; чим складніше це, тим краще), що дає бал для порівняння, який наступний крок є найбільш перспективним.

Тоді вам знадобляться лише такі частини:

  1. модель стану гри, в якій зберігається вся інформація про гру (наприклад, статус дошки / карта, черга, номер переміщення / положення в черзі)
  2. генератор руху, який дає всі дійсні ходи для даного ігрового стану
  3. функція "зробити переміщення" та "скасувати переміщення"; які застосовують / скасовують заданий (дійсний) перехід до ігрового стану. Тоді як функція "зробити переміщення" повинна зберігати деяку "інформацію про скасування" для функції "скасувати". Копіювання ігрового стану та змінення його в кожній ітерації значно сповільнює пошук! Спробуйте принаймні зберегти стан у стеці (= локальні змінні, не має динамічного розподілу за допомогою "нового").
  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" без подальше розслідування. Для цього вам знадобиться величезна хеш-таблиця, і вам доведеться приймати "хеш-колізії", що може призвести до того, що ви, мабуть, не знайдете існуючого рішення, але це малоймовірно, якщо ваші хеш-функції досить хороші, а ваша таблиця досить великий (це ризик розрахункового ризику).

Я думаю, що немає іншого алгоритму, який би вирішив цю проблему (як описано вами) більш ефективною, припускаючи, що ваша оцінка функції є оптимальною ...


Так, я можу знати повну чергу. Чи буде реалізація функції оцінювання також вважати правильним, але потенційно поганим? Можливо, поганий такий крок, як розміщення його поруч із квіткою іншого кольору, коли на полі вже є подібний колір? Або розмістити квітку десь, які блоки абсолютно іншого збігу через брак місця?
користувач849924

Ця відповідь дала мені ідеї щодо моделі та як працювати з ігровими правилами, тому я її схвалюю. Дякуємо за ваш внесок!
користувач849924

@ user849924: Так, звичайно, функція оцінювання повинна обчислити для цього оціночне "значення". Чим більше поточний стан гри стає гіршим (майже до втрати), тим гірше має бути повернене значення оцінки. Найпростішою оцінкою було б повернути кількість порожніх полів. Можна покращити це, додавши 0,1 для кожної квітки, розміщеної поруч із квіткою подібного кольору. Щоб перевірити свою функцію, виберіть деякі випадкові стани гри, обчисліть їх значення та порівняйте їх. Якщо ви думаєте, що стан A кращий, ніж стан B, оцінка fore A повинна бути кращою, ніж для B.
SDwarfs
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.