Який оптимальний алгоритм для гри 2048?


1919

Я нещодавно натрапив на гру 2048 року . Ви зливаєте подібні плитки, переміщуючи їх у будь-якому з чотирьох напрямків, щоб зробити "більші" плитки. Після кожного переміщення з'являється нова плитка у випадковому порожньому положенні зі значенням 2або 4. Гра закінчується, коли всі поля заповнені і немає жодних рухів, які можуть зливати плитки, або ви створюєте плитку зі значенням 2048.

По-перше, мені потрібно дотримуватися чітко визначеної стратегії, щоб досягти мети. Отже, я думав написати програму для цього.

Мій поточний алгоритм:

while (!game_over) {
    for each possible move:
        count_no_of_merges_for_2-tiles and 4-tiles
    choose the move with a large number of merges
}

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

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


84
Це може допомогти! ov3y.github.io/2048-AI
cegprakash

5
@ nitish712, до речі, ваш алгоритм жадібний, оскільки у вас choose the move with large number of mergesце швидко призводить до локальної
оптими

21
@ 500-InternalServerError: Якби я застосував AI з обрізкою дерева альфа-бета-ігор, було б припускати, що нові блоки розміщені в протилежному порядку. Це найгірше припущення, але може бути корисним.
Шарль

6
Весела відволікання, коли у вас немає часу націлитись на високий бал: Спробуйте отримати найнижчий можливий бал. Теоретично це чергування 2 та 4.
Марк Херд

7
Обговорення законності цього питання можна знайти на мета: meta.stackexchange.com/questions/227266/…
Jeroen Vannevel

Відповіді:


1266

Я розробив AI 2048 року, використовуючи оптимізацію очікувань , замість пошуку мінімакс, який використовується алгоритмом @ ovolve. AI просто виконує максимізацію за всіма можливими рухами з подальшим очікуванням на всі можливі нерестини плитки (зважені ймовірністю плиток, тобто 10% для 4 та 90% для 2). Наскільки мені відомо, не вдається обрізати очікування оптимізації оптимізації (за винятком видалення гілок, що малоймовірно), і тому використовуваний алгоритм - це ретельно оптимізований пошук грубої сили.

Продуктивність

AI в його конфігурації за замовчуванням (максимальна глибина пошуку 8) займає від 10 мс до 200 мс для виконання переміщення, залежно від складності положення дошки. Під час тестування AI досягає середньої швидкості руху 5-10 ходів за секунду протягом всієї гри. Якщо глибина пошуку обмежена 6 рухами, AI може легко виконувати 20+ рухів за секунду, що робить цікавим перегляд .

Щоб оцінити результативність AI, я запустив AI 100 разів (підключений до гри браузера за допомогою дистанційного керування). Для кожної плитки наведені пропорції ігор, в яких ця плитка була досягнута хоча б один раз:

2048: 100%
4096: 100%
8192: 100%
16384: 94%
32768: 36%

Мінімальний бал за всі прогони склав 124024; максимальний досягнутий бал становив 794076. Середній бал - 387222. AI ніколи не зміг отримати плитку 2048 (тому він ніколи не програвав гру навіть один раз у 100 іграх); насправді вона досягала плитки 8192 хоча б раз у раз!

Ось скріншот найкращого виконання:

32768 плитка, оцінка 794076

Ця гра займала 27830 рухів за 96 хвилин, або в середньому 4,8 ходу в секунду.

Впровадження

Мій підхід кодує всю дошку (16 записів) як єдине 64-бітне ціле число (де плитки - це nybbles, тобто 4-бітні шматки). На 64-бітному апараті це дозволяє передавати всю плату в єдиний реєстр машин.

Операції зсуву бітів використовуються для вилучення окремих рядків і стовпців. Один рядок або стовпець - це 16-бітна кількість, тому таблиця розміром 65536 може кодувати перетворення, які діють на один рядок або стовпець. Наприклад, ходи реалізуються у вигляді 4-х пошукових запитів у попередньо обчислену таблицю ефектів переміщення, яка описує, як кожен хід впливає на один рядок або стовпець (наприклад, таблиця "рухатись правою" містить запис "1122 -> 0023", описуючи, як рядок [2,2,4,4] стає рядком [0,0,4,8] при переміщенні праворуч).

Оцінка балів також проводиться за допомогою пошуку таблиць. У таблицях містяться евристичні оцінки, обчислені у всіх можливих рядках / стовпцях, а отриманий бал для дошки - це просто сума значень таблиці для кожного рядка та стовпця.

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

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

Евристика

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

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

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

Крім того, Петр також оптимізував евристичні ваги, використовуючи стратегію "метаоптимізації" (використовуючи алгоритм під назвою CMA-ES ), де самі ваги були скориговані для отримання максимально можливої ​​середньої оцінки.

Ефект від цих змін надзвичайно значний. Алгоритм пішов від досягнення плитки 16384 приблизно 13% часу до досягнення її понад 90% часу, і алгоритм почав досягати 32768 за 1/3 часу (тоді як стара евристика ніколи не виробляла плитку 32768) .

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


Те, що AI досягає 32768 плитки в більш ніж третині своїх ігор, - це величезна віха; Я буду здивований, почувши, чи хтось із гравців людини досягнув 32768 в офіційній грі (тобто без використання таких інструментів, як savestates або скасувати). Я думаю, що плитка 65536 знаходиться в межах досяжності!

Ви можете спробувати AI для себе. Код доступний на веб- сайті https://github.com/nneonneo/2048-ai .


12
@RobL: 2-х з’являються 90% часу; 4-х з'являються 10% часу. Це в вихідному коді : var value = Math.random() < 0.9 ? 2 : 4;.
nneonneo

35
Наразі портує до Cuda, щоб GPU працював на ще більші швидкості!
nimsson

25
@nneonneo Я переніс ваш код з emscripten на javascript, і він працює досить добре в браузері зараз! Прикольно дивитися, без необхідності збирати і все ... У Firefox продуктивність досить хороша ...
reverse_engineer

6
Теоретичний ліміт в сітці 4x4 насправді IS 131072, а не 65536. Однак для цього потрібно отримати 4 в потрібний момент (тобто вся дошка заповнена 4 .. 65536 кожен раз - 15 полів зайнята), і дошка повинна бути налаштована при цьому момент, щоб ви насправді могли поєднувати.
Бодо Тісен

5
@nneonneo Ви можете перевірити наш ІІ, який , здається , навіть краще, потрапляючи 32k в 60% ігор: github.com/aszczepanski/2048
Коші

1253

Я автор програми AI, яку інші згадували в цій темі. Ви можете переглянути AI в дії або прочитати джерело .

В даний час програма досягає приблизно 90% виграшу, який працює в javascript у браузері на моєму ноутбуці, враховуючи приблизно 100 мілісекунд часу на роздум за крок, тому, не будучи ідеальною (поки!), Вона працює досить добре.

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

Монотонність

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

Ось скріншот ідеально монотонної сітки. Я отримав це, запустивши алгоритм з функцією eval, встановленою для ігнорування інших евристик і лише врахування монотонності.

Ідеально монотонна дошка 2048 року

Гладкість

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

Коментолог "Hacker News" дав цікаву формалізацію цієї ідеї з точки зору теорії графів.

Ось скріншот ідеально гладкої сітки, ввічливість цієї чудової пародійної вилки .

Ідеально гладка дошка 2048 року

Безкоштовні плитки

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

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

Редагувати:

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

4096

Так, це 4096 поряд із 2048. =) Це означає, що він досягнув невловимого плитки 2048 року на одній дошці.


89
Ви можете ставитися до комп'ютера, розміщуючи плитки "2" та "4", як "противник".
Вей Єн

29
@WeiYen Безумовно, але розглядаючи це як проблему minmax не вірний логіці гри, оскільки комп'ютер розміщує плитки випадковим чином з певними ймовірностями, а не навмисно мінімізує рахунок.
коо

57
Незважаючи на те, що AI випадково розміщує плитки, мета - не програти. Отримати нещастя - це те саме, що опонент вибирає найгірший для вас хід. Частина "хв" означає, що ви намагаєтеся грати консервативно, щоб не було жахливих рухів, які могли б вам стати нещасливими.
FryGuy

196
У мене виникла ідея створити вилку 2048 року, де комп'ютер замість того, щоб розміщувати 2s і 4s випадковим чином, використовує ваш AI, щоб визначити, куди поставити значення. Результат: абсолютна неможливість. Можна спробувати тут: sztupy.github.io/2048-Hard
SztupY

30
@SztupY Ого, це зло. Нагадує мені про qntm.org/hatetris Hatetris, який також намагається розмістити твір, який як мінімум покращить вашу ситуацію.
Паташу

145

Мене зацікавила ідея AI для цієї гри, що не містить жорсткого коду інтелекту (тобто немає евристики, бальних функцій тощо). ШІ повинен «знати» лише правила гри та «розгадувати» гру. Це на відміну від більшості AI (на зразок тих, що знаходяться в цій темі), де гра є по суті грубою силою, керованою функцією забиття, що представляє людське розуміння гри.

Алгоритм А.І.

Я знайшов простий, але напрочуд хороший алгоритм гри: Щоб визначити наступний хід для даної дошки, AI грає в гру в пам'яті, використовуючи випадкові рухи, поки гра не закінчиться. Це робиться кілька разів, відстежуючи кінцевий рахунок гри. Потім обчислюється середній кінцевий бал за стартовий хід . Стартовий хід з найвищим середнім кінцевим балом вибирається як наступний хід.

Маючи всього 100 пробіжок (тобто в іграх на пам'ять) за хід, AI досягає плитки 2048 у 80% разів, а плитка 4096 - у 50% разів. Використовуючи 10000 пробіг, плитка 2048 отримує 100%, 70% - 4096 плитки, і близько 1% - для плитки 8192.

Побачити це в дії

Найкращий досягнутий бал показаний тут:

найкращий рахунок

Цікавим фактом щодо цього алгоритму є те, що хоча ігри з випадковими іграми не дивно досить погані, вибір найкращого (або щонайменше поганого) ходу призводить до дуже хорошої гри: Типова гра AI може досягти 70000 очок та останні 3000 рухів, але Ігри в випадковій пам'яті з будь-якої заданої позиції приносять в середньому 340 додаткових очок приблизно за 40 додаткових ходів до смерті. (Ви можете переконатися в цьому, запустивши AI і відкривши конфігурацію налагодження.)

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

графік балів

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

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

Впровадження та посилання

Спочатку я створив версію JavaScript, яку можна побачити тут . Ця версія може виконати 100 пробігів у пристойний час. Відкрийте консоль для отримання додаткової інформації. ( джерело )

Пізніше, щоб пограти ще трохи, я використав @nneonneo високооптимізовану інфраструктуру та впровадив свою версію в C ++. Ця версія дозволяє робити до 100000 пробігів за ходу і навіть 1000000, якщо у вас є терпіння. Надані інструкції щодо будівництва. Він працює в консолі, а також має пульт управління для відтворення веб-версії. ( джерело )

Результати

Дивно, але збільшення кількості пробіжок суттєво не покращує гру. Здається, існує обмеження в цій стратегії приблизно в 80000 пунктів, коли плитка 4096 та всі менші розміри є дуже близькими до досягнення плитки 8192. Збільшення кількості пробіжок від 100 до 100000 збільшує шанси дійти до цієї межі балів (з 5% до 40%), але не прорватися через неї.

Виконуючи 10000 пробігів з тимчасовим збільшенням до 1000000 поблизу критичних позицій, вдалося зламати цей бар’єр менше ніж у 1% разів, досягнувши максимального балу 129892 та плитки 8192.

Поліпшення

Реалізувавши цей алгоритм, я спробував багато вдосконалень, включаючи використання мінімальних або максимальних балів, або комбінацію min, max та avg. Я також спробував використовувати глибину: Замість того , щоб намагатися пробігами K на хід, я спробував K ходів в ході списку заданою довжиною ( «вгору, вгору, ліворуч», наприклад) , і вибравши перший хід списку кращого скоринг рухатися.

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

Однак жодна з цих ідей не показала реальної переваги перед простою першою ідеєю. Я залишив код для цих ідей, прокоментований у коді C ++.

Я додав механізм "Глибокого пошуку", який тимчасово збільшив кількість запуску до 1000000, коли будь-який з пробіг вдалося випадково досягти наступної найвищої плитки. Це запропонувало покращити час.

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

2048 Варіанти та клони

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

Це можливо завдяки доменно-незалежному характеру ШІ. Деякі варіанти є досить чіткими, наприклад, шестикутний клон.


7
+1. Як студент AI мені це було цікаво. Докладно розглянемо це у вільний час.
Ісаак

4
Це дивно! Я щойно витратив години на оптимізацію ваг для гарної евристичної функції для очікувань, і я реалізую це за 3 хвилини, і це повністю розбиває його.
Брендан Аннабель

8
Хороше використання моделювання Монте-Карло.
nneonneo

5
Перегляд цієї гри закликає до просвітлення. Це підірває всю евристику, але вона працює. Вітаємо!
Стефан Гурішон

4
На сьогодні найцікавіше рішення.
shebaw

126

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

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

Готовий до кінця

Це модель, яку я вибрав за замовчуванням.

1024 512 256 128
  8   16  32  64
  4   2   x   x
  x   x   x   x

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

Тут іде алгоритм. Близько 80% виграшів (мабуть, завжди можна виграти за допомогою "професійніших" методів AI, я не впевнений у цьому.)

initiateModel();

while(!game_over)
{    
    checkCornerChosen(); // Unimplemented, but it might be an improvement to change the reference point

    for each 3 possible move:
        evaluateResult()
    execute move with best score
    if no move is available, execute forbidden move and undo, recalculateModel()
 }

 evaluateResult() {
     calculatesBestCurrentModel()
     calculates distance to chosen model
     stores result
 }

 calculateBestCurrentModel() {
      (according to the current highest tile acheived and their distribution)
  }

Кілька покажчиків на пропущені щаблі. Тут:зміна моделі

Модель змінилася через удачу бути ближче до очікуваної моделі. Модель, яку намагається досягти AI, є

 512 256 128  x
  X   X   x   x
  X   X   x   x
  x   x   x   x

І ланцюжок доїхати стала:

 512 256  64  O
  8   16  32  O
  4   x   x   x
  x   x   x   x

OПредставляють заборонені місця ...

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

Ланцюг завершено

Тепер модель і ланцюг повернулися до:

 512 256 128  64
  4   8  16   32
  X   X   x   x
  x   x   x   x

Другий вказівник, йому пощастило, і його головне місце було зайнято. Цілком імовірно, що він вийде з ладу, але все ж може досягти цього:

Введіть тут опис зображення

Тут модель і ланцюг:

  O 1024 512 256
  O   O   O  128
  8  16   32  64
  4   x   x   x

Коли йому вдається досягти 128, він знову отримує цілий ряд:

  O 1024 512 256
  x   x  128 128
  x   x   x   x
  x   x   x   x

execute move with best scoreяк можна оцінити найкращий бал з можливих наступних станів?
Халед.К

евристика визначена у evaluateResultвас, в основному намагайтеся наблизитися до найкращого можливого сценарію.
Дарен

@Daren Я чекаю вашої детальної конкретики
ашу

@ashu Я працюю над цим, несподівані обставини залишили мене без часу закінчити. Тим часом я вдосконалив алгоритм, і він зараз вирішує його 75% часу.
Дарен

13
Що мені дуже подобається в цій стратегії, це те, що я в змозі використовувати її під час гри вручну, вона отримала мені до 37k балів.
Головоноги

94

Я копіюю тут вміст публікації у своєму блозі


Я пропоную рішення дуже просте і просте у виконанні. Хоча вона досягла оцінки 131040. Представлено кілька орієнтирів виконання алгоритму.

Оцінка

Алгоритм

Евристичний алгоритм оцінювання

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

(Існує можливість дістатися до 131072 плитки, якщо 4-х плитка випадково генерується замість 2-х плиток, коли це потрібно)

Два наступні способи організації дошки показані на наступних зображеннях:

введіть тут опис зображення

Для примусового впорядкування плиток у монотонному порядку зменшення бал si обчислюється як сума лінеаризованих значень на дошці, помножена на значення геометричної послідовності із загальним співвідношенням r <1.

с

с

Одразу можна оцінити декілька лінійних шляхів, остаточний результат буде максимальним балом будь-якого шляху.

Правило рішення

Реалізоване правило рішення не зовсім розумне, код на Python представлений тут:

@staticmethod
def nextMove(board,recursion_depth=3):
    m,s = AI.nextMoveRecur(board,recursion_depth,recursion_depth)
    return m

@staticmethod
def nextMoveRecur(board,depth,maxDepth,base=0.9):
    bestScore = -1.
    bestMove = 0
    for m in range(1,5):
        if(board.validMove(m)):
            newBoard = copy.deepcopy(board)
            newBoard.move(m,add_tile=True)

            score = AI.evaluate(newBoard)
            if depth != 0:
                my_m,my_s = AI.nextMoveRecur(newBoard,depth-1,maxDepth)
                score += my_s*pow(base,maxDepth-depth+1)

            if(score > bestScore):
                bestMove = m
                bestScore = score
    return (bestMove,bestScore);

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

Орієнтир

  • T1 - 121 тест - 8 різних шляхів - r = 0,125
  • T2 - 122 тести - 8-різних шляхів - r = 0,25
  • T3 - 132 тестів - 8-різних шляхів - r = 0,5
  • Т4 - 211 тестів - 2-різні шляхи - r = 0,125
  • T5 - 274 тестів - 2-різні шляхи - r = 0,25
  • T6 - 211 тестів - 2-різні шляхи - r = 0,5

введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення

У випадку T2 чотири тести з десяти генерують плитку 4096 із середньою оцінкою с42000

Код

Код можна знайти на GiHub за наступним посиланням: https://github.com/Nicola17/term2048-AI Він заснований на терміні2048 і написаний на Python. Я буду впроваджувати більш ефективну версію в C ++ якнайшвидше.


Непогано, ваша ілюстрація дала мені ідею взяти оцінку векторів злиття
Khaled.K

Привіт. Ви впевнені, що інструкції, наведені на сторінці github, стосуються вашого проекту? Я хочу спробувати це, але, здається, це інструкція до оригінальної відтворюваної гри, а не автозапуск AI. Чи можете ви оновити їх? Дякую.
JD Gamboa

41

У моїй спробі використовується очакванийкс, як і інші рішення вище, але без бітбордів. Рішення Nneonneo може перевірити 10 мільйонів ходів, що приблизно на глибині 4, з 6 плитками і 4 можливими ходами (2 * 6 * 4) 4 . У моєму випадку ця глибина займає занадто багато часу, щоб досліджувати, я регулюю глибину очікуваннямаксимального пошуку відповідно до кількості вільних плиток:

depth = free > 7 ? 1 : (free > 4 ? 2 : 3)

Оцінки дощок обчислюються із зваженою сумою квадрата кількості вільних плиток та крапкового добутку 2D сітки з таким:

[[10,8,7,6.5],
 [.5,.7,1,3],
 [-.5,-1.5,-1.8,-2],
 [-3.8,-3.7,-3.5,-3]]

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

код нижче або на github :

var n = 4,
	M = new MatrixTransform(n);

var ai = {weights: [1, 1], depth: 1}; // depth=1 by default, but we adjust it on every prediction according to the number of free tiles

var snake= [[10,8,7,6.5],
            [.5,.7,1,3],
            [-.5,-1.5,-1.8,-2],
            [-3.8,-3.7,-3.5,-3]]
snake=snake.map(function(a){return a.map(Math.exp)})

initialize(ai)

function run(ai) {
	var p;
	while ((p = predict(ai)) != null) {
		move(p, ai);
	}
	//console.log(ai.grid , maxValue(ai.grid))
	ai.maxValue = maxValue(ai.grid)
	console.log(ai)
}

function initialize(ai) {
	ai.grid = [];
	for (var i = 0; i < n; i++) {
		ai.grid[i] = []
		for (var j = 0; j < n; j++) {
			ai.grid[i][j] = 0;
		}
	}
	rand(ai.grid)
	rand(ai.grid)
	ai.steps = 0;
}

function move(p, ai) { //0:up, 1:right, 2:down, 3:left
	var newgrid = mv(p, ai.grid);
	if (!equal(newgrid, ai.grid)) {
		//console.log(stats(newgrid, ai.grid))
		ai.grid = newgrid;
		try {
			rand(ai.grid)
			ai.steps++;
		} catch (e) {
			console.log('no room', e)
		}
	}
}

function predict(ai) {
	var free = freeCells(ai.grid);
	ai.depth = free > 7 ? 1 : (free > 4 ? 2 : 3);
	var root = {path: [],prob: 1,grid: ai.grid,children: []};
	var x = expandMove(root, ai)
	//console.log("number of leaves", x)
	//console.log("number of leaves2", countLeaves(root))
	if (!root.children.length) return null
	var values = root.children.map(expectimax);
	var mx = max(values);
	return root.children[mx[1]].path[0]

}

function countLeaves(node) {
	var x = 0;
	if (!node.children.length) return 1;
	for (var n of node.children)
		x += countLeaves(n);
	return x;
}

function expectimax(node) {
	if (!node.children.length) {
		return node.score
	} else {
		var values = node.children.map(expectimax);
		if (node.prob) { //we are at a max node
			return Math.max.apply(null, values)
		} else { // we are at a random node
			var avg = 0;
			for (var i = 0; i < values.length; i++)
				avg += node.children[i].prob * values[i]
			return avg / (values.length / 2)
		}
	}
}

function expandRandom(node, ai) {
	var x = 0;
	for (var i = 0; i < node.grid.length; i++)
		for (var j = 0; j < node.grid.length; j++)
			if (!node.grid[i][j]) {
				var grid2 = M.copy(node.grid),
					grid4 = M.copy(node.grid);
				grid2[i][j] = 2;
				grid4[i][j] = 4;
				var child2 = {grid: grid2,prob: .9,path: node.path,children: []};
				var child4 = {grid: grid4,prob: .1,path: node.path,children: []}
				node.children.push(child2)
				node.children.push(child4)
				x += expandMove(child2, ai)
				x += expandMove(child4, ai)
			}
	return x;
}

function expandMove(node, ai) { // node={grid,path,score}
	var isLeaf = true,
		x = 0;
	if (node.path.length < ai.depth) {
		for (var move of[0, 1, 2, 3]) {
			var grid = mv(move, node.grid);
			if (!equal(grid, node.grid)) {
				isLeaf = false;
				var child = {grid: grid,path: node.path.concat([move]),children: []}
				node.children.push(child)
				x += expandRandom(child, ai)
			}
		}
	}
	if (isLeaf) node.score = dot(ai.weights, stats(node.grid))
	return isLeaf ? 1 : x;
}



var cells = []
var table = document.querySelector("table");
for (var i = 0; i < n; i++) {
	var tr = document.createElement("tr");
	cells[i] = [];
	for (var j = 0; j < n; j++) {
		cells[i][j] = document.createElement("td");
		tr.appendChild(cells[i][j])
	}
	table.appendChild(tr);
}

function updateUI(ai) {
	cells.forEach(function(a, i) {
		a.forEach(function(el, j) {
			el.innerHTML = ai.grid[i][j] || ''
		})
	});
}


updateUI(ai);
updateHint(predict(ai));

function runAI() {
	var p = predict(ai);
	if (p != null && ai.running) {
		move(p, ai);
		updateUI(ai);
		updateHint(p);
		requestAnimationFrame(runAI);
	}
}
runai.onclick = function() {
	if (!ai.running) {
		this.innerHTML = 'stop AI';
		ai.running = true;
		runAI();
	} else {
		this.innerHTML = 'run AI';
		ai.running = false;
		updateHint(predict(ai));
	}
}


function updateHint(dir) {
	hintvalue.innerHTML = ['↑', '→', '↓', '←'][dir] || '';
}

document.addEventListener("keydown", function(event) {
	if (!event.target.matches('.r *')) return;
	event.preventDefault(); // avoid scrolling
	if (event.which in map) {
		move(map[event.which], ai)
		console.log(stats(ai.grid))
		updateUI(ai);
		updateHint(predict(ai));
	}
})
var map = {
	38: 0, // Up
	39: 1, // Right
	40: 2, // Down
	37: 3, // Left
};
init.onclick = function() {
	initialize(ai);
	updateUI(ai);
	updateHint(predict(ai));
}


function stats(grid, previousGrid) {

	var free = freeCells(grid);

	var c = dot2(grid, snake);

	return [c, free * free];
}

function dist2(a, b) { //squared 2D distance
	return Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)
}

function dot(a, b) {
	var r = 0;
	for (var i = 0; i < a.length; i++)
		r += a[i] * b[i];
	return r
}

function dot2(a, b) {
	var r = 0;
	for (var i = 0; i < a.length; i++)
		for (var j = 0; j < a[0].length; j++)
			r += a[i][j] * b[i][j]
	return r;
}

function product(a) {
	return a.reduce(function(v, x) {
		return v * x
	}, 1)
}

function maxValue(grid) {
	return Math.max.apply(null, grid.map(function(a) {
		return Math.max.apply(null, a)
	}));
}

function freeCells(grid) {
	return grid.reduce(function(v, a) {
		return v + a.reduce(function(t, x) {
			return t + (x == 0)
		}, 0)
	}, 0)
}

function max(arr) { // return [value, index] of the max
	var m = [-Infinity, null];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] > m[0]) m = [arr[i], i];
	}
	return m
}

function min(arr) { // return [value, index] of the min
	var m = [Infinity, null];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] < m[0]) m = [arr[i], i];
	}
	return m
}

function maxScore(nodes) {
	var min = {
		score: -Infinity,
		path: []
	};
	for (var node of nodes) {
		if (node.score > min.score) min = node;
	}
	return min;
}


function mv(k, grid) {
	var tgrid = M.itransform(k, grid);
	for (var i = 0; i < tgrid.length; i++) {
		var a = tgrid[i];
		for (var j = 0, jj = 0; j < a.length; j++)
			if (a[j]) a[jj++] = (j < a.length - 1 && a[j] == a[j + 1]) ? 2 * a[j++] : a[j]
		for (; jj < a.length; jj++)
			a[jj] = 0;
	}
	return M.transform(k, tgrid);
}

function rand(grid) {
	var r = Math.floor(Math.random() * freeCells(grid)),
		_r = 0;
	for (var i = 0; i < grid.length; i++) {
		for (var j = 0; j < grid.length; j++) {
			if (!grid[i][j]) {
				if (_r == r) {
					grid[i][j] = Math.random() < .9 ? 2 : 4
				}
				_r++;
			}
		}
	}
}

function equal(grid1, grid2) {
	for (var i = 0; i < grid1.length; i++)
		for (var j = 0; j < grid1.length; j++)
			if (grid1[i][j] != grid2[i][j]) return false;
	return true;
}

function conv44valid(a, b) {
	var r = 0;
	for (var i = 0; i < 4; i++)
		for (var j = 0; j < 4; j++)
			r += a[i][j] * b[3 - i][3 - j]
	return r
}

function MatrixTransform(n) {
	var g = [],
		ig = [];
	for (var i = 0; i < n; i++) {
		g[i] = [];
		ig[i] = [];
		for (var j = 0; j < n; j++) {
			g[i][j] = [[j, i],[i, n-1-j],[j, n-1-i],[i, j]]; // transformation matrix in the 4 directions g[i][j] = [up, right, down, left]
			ig[i][j] = [[j, i],[i, n-1-j],[n-1-j, i],[i, j]]; // the inverse tranformations
		}
	}
	this.transform = function(k, grid) {
		return this.transformer(k, grid, g)
	}
	this.itransform = function(k, grid) { // inverse transform
		return this.transformer(k, grid, ig)
	}
	this.transformer = function(k, grid, mat) {
		var newgrid = [];
		for (var i = 0; i < grid.length; i++) {
			newgrid[i] = [];
			for (var j = 0; j < grid.length; j++)
				newgrid[i][j] = grid[mat[i][j][k][0]][mat[i][j][k][1]];
		}
		return newgrid;
	}
	this.copy = function(grid) {
		return this.transform(3, grid)
	}
}
body {
	font-family: Arial;
}
table, th, td {
	border: 1px solid black;
	margin: 0 auto;
	border-collapse: collapse;
}
td {
	width: 35px;
	height: 35px;
	text-align: center;
}
button {
	margin: 2px;
	padding: 3px 15px;
	color: rgba(0,0,0,.9);
}
.r {
	display: flex;
	align-items: center;
	justify-content: center;
	margin: .2em;
	position: relative;
}
#hintvalue {
	font-size: 1.4em;
	padding: 2px 8px;
	display: inline-flex;
	justify-content: center;
	width: 30px;
}
<table title="press arrow keys"></table>
<div class="r">
    <button id=init>init</button>
    <button id=runai>run AI</button>
    <span id="hintvalue" title="Best predicted move to do, use your arrow keys" tabindex="-1"></span>
</div>


3
Не впевнений, чому в цьому немає більше відгуків. Це дійсно ефективно, оскільки це простота.
Давид Грейданус

Дякую, пізня відповідь, і вона працює не дуже добре (майже завжди в [1024, 8192]), функція витрат / статистики потребує більшої роботи
кают

Як ви зважили порожні місця?
Давид Грейданус

1
Це просто, cost=1x(number of empty tiles)²+1xdotproduct(snakeWeights,grid)і ми намагаємося максимально збільшити цю вартість
кабіна

дякую @Robusto, я повинен вдосконалити код якийсь день, його можна спростити
caub

38

Я автор контролера 2048, який набирає кращих результатів, ніж будь-яка інша програма, згадана в цій темі. Ефективна реалізація контролера доступна на github . В окремому репо є також код, який використовується для навчання функції оцінювання стану контролера. Метод навчання описаний у статті .

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

Продуктивність

За 1 хід / с: 609104 (в середньому 100 ігор)

При 10 ходах / с: 589355 (в середньому 300 ігор)

На 3 плані (приблизно 1500 рухів / с): шарах ( 511759 (в середньому 1000 ігор)

Статистика плитки за 10 ходів / с така:

2048: 100%
4096: 100%
8192: 100%
16384: 97%
32768: 64%
32768,16384,8192,4096: 10%

(Останній рядок означає мати одночасно вказані плитки на дошці).

Для тришарових:

2048: 100%
4096: 100%
8192: 100%
16384: 96%
32768: 54%
32768,16384,8192,4096: 8%

Однак я ніколи не спостерігав за тим, щоб отримати плитку 65536.


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

27

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

Будь ласка, дивіться код нижче:

while( !game_over ) {
    move_direction=up;
    if( !move_is_possible(up) ) {
        if( move_is_possible(right) && move_is_possible(left) ){
            if( number_of_empty_cells_after_moves(left,up) > number_of_empty_cells_after_moves(right,up) ) 
                move_direction = left;
            else
                move_direction = right;
        } else if ( move_is_possible(left) ){
            move_direction = left;
        } else if ( move_is_possible(right) ){
            move_direction = right;
        } else {
            move_direction = down;
        }
    }
    do_move(move_direction);
}

5
Я провів 100 000 ігор, тестуючи це проти тривіальної циклічної стратегії "вгору, вправо, вгору, вліво, ..." (і вниз, якщо треба). Циклічна стратегія закінчила "середній бал плитки" 770.6, тоді як цей отримав справедливий 396.7. Чи здогадуєтесь ви, чому це може бути? Я думаю, що це робить занадто багато підйомів, навіть коли лівий або правий збільшиться набагато більше.
Томас Ейл

1
Плитки, як правило, укладаються несумісними способами, якщо вони не зміщені в декілька напрямків. Взагалі, використання циклічної стратегії призведе до збільшення великої плитки в центрі, що зробить маневрування набагато більш тісним.
bcdan

25

Існує вже реалізація ІІ для цієї гри тут . Витяг з README:

Алгоритм - це ітеративне поглиблення глибини першого пошуку альфа-бета. Функція оцінювання намагається зберегти монотонні рядки та стовпці (або всі зменшуються або збільшуються), мінімізуючи кількість плиток у сітці.

На Hacker News також є дискусія щодо цього алгоритму, який може вам бути корисним.


4
Це має бути головна відповідь, але було б непогано додати більше деталей щодо реалізації: наприклад, моделювання ігрової дошки (як графік), оптимізація, що застосовується (min-max різниця між плитками) тощо
Alceu Costa

1
Для майбутніх читачів: Це та сама програма, яку пояснив її автор (ovolve) у другій відповіді тут. Ця відповідь та інші згадки про програму ovolve в цій дискусії спонукали ovolve з'явитися і написати, як працює його алгоритм; ця відповідь тепер має оцінку 1200.
MultiplyByZer0

23

Алгоритм

while(!game_over)
{
    for each possible move:
        evaluate next state

    choose the maximum evaluation
}

Оцінка

Evaluation =
    128 (Constant)
    + (Number of Spaces x 128)
    + Sum of faces adjacent to a space { (1/face) x 4096 }
    + Sum of other faces { log(face) x 4 }
    + (Number of possible next moves x 256)
    + (Number of aligned values x 2)

Деталі оцінки

128 (Constant)

Це константа, яка використовується як базова лінія та для інших цілей, таких як тестування.

+ (Number of Spaces x 128)

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

+ Sum of faces adjacent to a space { (1/face) x 4096 }

Тут ми оцінюємо грані, які мають можливість злиття, оцінюючи їх назад, плитка 2 набуває значення 2048, тоді як плитка 2048 оцінюється 2.

+ Sum of other faces { log(face) x 4 }

Тут нам ще потрібно перевірити наявність складених значень, але меншою мірою, що не перебиває параметри гнучкості, тому у нас є сума {x в [4,44]}.

+ (Number of possible next moves x 256)

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

+ (Number of aligned values x 2)

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

Примітка. Константи можна налаштувати ..


2
Я відредагую це пізніше, щоб додати прямий код @ nitish712
Khaled.K

9
Який виграшний% цього алгоритму?
cegprakash

Для чого вам потрібен constant? Якщо ви все, що робите, порівнюєте бали, як це впливає на результат цих порівнянь?
bcdan

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

12

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

Я щойно спробував реалізувати minimax з обрізкою альфа-бета з обрізанням глибини пошукового дерева на 3 та 5. Я намагався вирішити ту саму проблему для сітки 4x4, як проектне завдання для курсу edX ColumbiaX: CSMM.101x Artificial Intelligence ( AI) .

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

  1. Монотонність
  2. Вільний простір

У моєму випадку комп'ютерний плеєр є абсолютно випадковим, але все ж я припустив змагальні налаштування і реалізував агент гравця AI як максимальний гравець.

У мене є 4х4 сітка для гри.

Спостереження:

Якщо я присвоюю занадто велику вагу першій евристичній функції або другій евристичній функції, обидва випадки балів, які отримує AI-гравець, є низькими. Я грав з багатьма можливими привласненнями ваги евристичним функціям і приймаю опуклу комбінацію, але дуже рідко гравець AI здатний набрати 2048. У більшості випадків він або зупиняється на 1024 або 512.

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

Крім того, я спробував збільшити глибину пошуку з 3 до 5 (я не можу збільшити її більше, оскільки пошук у тому, що простір перевищує дозволений час навіть під час обрізки), і додав ще одне евристичне, яке розглядає значення сусідніх плиток і дає більше очок, якщо вони можуть бути об'єднаними, але все-таки я не в змозі отримати 2048 рік.

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

Нижче анімація показує останні кілька кроків гри, яку грає агент AI разом із комп'ютерним плеєром:

введіть тут опис зображення

Будь-яка інформація буде дуже корисною, заздалегідь дякую. (Це посилання на мій пост у блозі до статті: https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve -2048-гра-з комп’ютером / та відео YouTube: https://www.youtube.com/watch?v=VnVFilfZ0r4 )

Наступна анімація показує останні кілька кроків гри, в якій агент гравця AI може отримати 2048 балів, на цей раз додаючи також евристичне значення абсолютного значення:

введіть тут опис зображення

На наступних малюнках показано ігрове дерево, досліджене агентом AI гравця, вважаючи, що комп'ютер є противником лише за один крок:

введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення


9

Я написав вирішувач 2048 року в Haskell, головним чином тому, що я зараз вивчаю цю мову.

Моя реалізація гри трохи відрізняється від фактичної гри тим, що нова плитка завжди є «2» (а не 90% 2 та 10% 4). І що нова плитка не випадкова, а завжди перша доступна зверху зліва. Цей варіант також відомий як Det 2048 .

Як наслідок, цей вирішувач є детермінованим.

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

Нижче наведено код, що реалізує алгоритм вирішення. Сітка представлена ​​у вигляді 16-довжинного масиву Цілих чисел. А підрахунок проводиться просто шляхом підрахунку кількості порожніх квадратів.

bestMove :: Int -> [Int] -> Int
bestMove depth grid = maxTuple [ (gridValue depth (takeTurn x grid), x) | x <- [0..3], takeTurn x grid /= [] ]

gridValue :: Int -> [Int] -> Int
gridValue _ [] = -1
gridValue 0 grid = length $ filter (==0) grid  -- <= SCORING
gridValue depth grid = maxInList [ gridValue (depth-1) (takeTurn x grid) | x <- [0..3] ]

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

Move 4006
[2,64,16,4]
[16,4096,128,512]
[2048,64,1024,16]
[2,4,16,2]

Game Over

Вихідний код можна знайти тут: https://github.com/popovitsj/2048-haskell


Спробуйте розширити його власне правилами. Це гарне завдання дізнатися про випадковий генератор Haskell!
Томас Ейл

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

Без рандомізації я впевнений, що ви можете знайти спосіб отримати 16 к або 32 к. Однак рандомізація в Haskell не така вже й погана, вам просто потрібен спосіб пройти навколо "насіння". Або робити це явно, або з випадковою монадою.
Thomas Ahle

Вдосконалення алгоритму так, щоб він завжди досягав 16 к / 32 к для невипадкової гри, може бути ще однією цікавою проблемою ...
wvdz

Ви маєте рацію, це важче, ніж я думав. Мені вдалося знайти таку послідовність: [Вгору, Вліво, Вліво, Вгору, Вліво, Вниз, Вліво], яка завжди виграє гру, але вона не перевищує 2048 року. наступний за порядком годинникової стрілки)
Thomas Ahle

6

Цей алгоритм не є оптимальним для виграшу в грі, але є досить оптимальним щодо продуктивності та кількості необхідного коду:

  if(can move neither right, up or down)
    direction = left
  else
  {
    do
    {
      direction = random from (right, down, up)
    }
    while(can not move in "direction")
  }

10
краще, якщо ви скажете, що random from (right, right, right, down, down, up) не всі ходи мають однакову ймовірність. :)
Дарен

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

5
Так, це засноване на моєму власному спостереженні за грою. Поки вам не доведеться використовувати 4-й напрямок, гра практично вирішить себе без будь-якого спостереження. Цей "AI" повинен мати можливість дістатися до 512/1024, не перевіряючи точне значення будь-якого блоку.
API-Beast

3
Правильний ШІ намагатиметься уникати стану, коли за будь-яку ціну він може рухатися лише в одному напрямку.
API-Beast

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

4

Багато інших відповідей використовують AI для обчислювально дорогого пошуку можливих ф’ючерсів, евристики, навчання та іншого. Це вражаючий і, мабуть, правильний шлях вперед, але я хочу внести ще одну ідею.

Моделюйте стратегію, яку використовують хороші гравці гри.

Наприклад:

13 14 15 16
12 11 10  9
 5  6  7  8
 4  3  2  1

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

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


Плитка потребує злиття з сусідом, але вона занадто мала: з’єднайте іншого сусіда з цим.

Більша плитка за способом: збільшуйте значення меншої навколишньої плитки.

тощо ...


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


5
Ви описуєте локальний пошук за допомогою евристики. Це засмітить вас, тому вам потрібно планувати заздалегідь наступні кроки. Це, в свою чергу, призводить і до пошуку, і до вибору рішень (щоб прийняти рішення). Тож це насправді не відрізняється від будь-якого іншого представленого рішення.
runDOSrun
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.