Каков оптимальный алгоритм игры 2048?

Недавно я наткнулся на игру 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 очков, что намного больше, чем мой текущий балл. Есть ли лучший алгоритм, чем выше?

1718
задан nitish712 12 марта '14 в 8:37
источник поделиться

14 ответов

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

Производительность

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

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

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

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

Вот скриншот лучшего запуска:

32768 tile, score 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 года ноутбук).

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

эвристика

Несколько эвристик используются для направления алгоритма оптимизации на выгодные позиции. Точный выбор эвристики оказывает огромное влияние на производительность алгоритма. Различные эвристики взвешиваются и объединяются в позиционный балл, который определяет, насколько "хороша" данная позиция на борту. Затем поиск по оптимизации нацелен на максимизацию среднего балла всех возможных позиций на борту. Фактический балл, как показано в игре, не используется для вычисления оценки доски, поскольку он слишком сильно взвешен в пользу слияния плиток (когда отложенное слияние может принести большую пользу).

Вначале я использовал две очень простые эвристики, предоставляя "бонусы" для открытых квадратов и для больших значений на краю. Эти эвристики выполнялись довольно хорошо, часто достигая 16384, но никогда не добирались до 32768.

Петр Моравек (@xificurk) взял мой ИИ и добавил две новые эвристики. Первая эвристика была штрафом за немонотонные строки и столбцы, которые увеличивались по мере увеличения рангов, гарантируя, что немонотонные строки небольших чисел не будут сильно влиять на оценку, но немонотонные ряды больших чисел существенно повредят. Вторая эвристика подсчитала количество потенциальных слияний (смежных равных значений) в дополнение к открытым пространствам. Эти две эвристики служили для того, чтобы подтолкнуть алгоритм к монотонным доскам (которые легче слить) и к посадкам с большим количеством слияний (поощряя его выравнивать слияния, где это возможно, для большего эффекта).

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

Эффект этих изменений чрезвычайно важен. Алгоритм пошел от достижения плитки 16384 около 13% времени, чтобы достичь ее более 90% времени, и алгоритм начал добиваться 32768 в течение 1/3 времени (тогда как старые эвристики никогда не производили 32768 плит).

Я считаю, что есть еще место для улучшения эвристики. Этот алгоритм определенно еще не "оптимален", но я чувствую, что он приближается.


То, что ИИ достигает 32768 плит в более чем одной трети своих игр, является огромной вехой; Я буду удивлен, если услышу, достигли ли какие-либо игроки в игре 32768 в официальной игре (т.е. Без использования таких инструментов, как savestates или cancel). Я думаю, что плитка 65536 находится в пределах досягаемости!

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

1103
ответ дан nneonneo 19 марта '14 в 10:22
источник поделиться

Я автор программы AI, которую другие упомянули в этой теме. Вы можете просмотреть AI в action или прочитать источник.

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

Поскольку игра представляет собой дискретное пространство состояний, совершенную информацию, пошаговую игру, такую ​​как шахматы и шашки, я использовал те же методы, которые были доказаны для работы над этими играми, а именно minimax поиск с alpha- бета-обрезка. Поскольку в этом алгоритме уже есть много информации, я просто расскажу о двух основных эвристиках, которые я использую в статической функции оценки и которые формализуют многие интуиции, которые здесь выражали другие люди.

монотонности

Эта эвристика пытается убедиться, что значения плиток все либо увеличиваются, либо уменьшаются вдоль обоих направлений влево/вправо и вверх/вниз. Только эта эвристика отражает интуицию, о которой говорили многие другие, о том, что более высокоценные плитки должны быть сгруппированы в углу. Это, как правило, предотвращает получение сиротских ценных бумаг с более мелкой ценностью и будет держать плату очень организованной, а мелкие плитки будут каскадироваться и заполняться большими плитами.

Вот скриншот совершенно монотонной сетки. Я получил это, запустив алгоритм с установленной функцией eval, чтобы игнорировать другие эвристики и рассматривать только монотонность.

A perfectly monotonic 2048 board

Плавность

Вышеупомянутая эвристика сама по себе имеет тенденцию создавать структуры, в которых смежные плитки снижаются по стоимости, но, конечно, для слияния соседние плитки должны быть одинаковыми. Поэтому эвристика гладкости просто измеряет разницу значений между соседними плитами, пытаясь свести к минимуму этот счет.

Комментарии от Hacker News дали интересную формализацию этой идеи в терминах теории графов.

Вот скриншот совершенно гладкой сетки, любезно предоставленной этой отличной вилкой для пародий.

A perfectly smooth 2048 board

Свободные плитки

И, наконец, есть штраф за то, что у него слишком мало свободных фрагментов, так как параметры могут быстро заканчиваться, когда игровая панель становится слишком стесненной.

И это! Поиск в игровом пространстве при оптимизации этих критериев дает замечательную производительность. Одним из преимуществ использования обобщенного подхода, подобного этой, а не явно закодированной стратегии перемещения, является то, что алгоритм может часто находить интересные и неожиданные решения. Если вы посмотрите, как он бежит, он часто будет делать неожиданные, но эффективные движения, например, внезапно переключая ту стену или угол, в которых она создается.

Изменить:

Здесь демонстрируется сила этого подхода. Я отключил значения плитки (поэтому он продолжал идти после достижения 2048), и вот лучший результат после восьми испытаний.

4096

Да, это 4096 вместе с 2048. =) Это означает, что трижды проделала неуловимую плиту 2048 на одной доске.

1218
ответ дан ovolve 13 марта '14 в 23:04
источник поделиться

EDIT:. Это наивный алгоритм, моделирующий процесс сознательного мышления человека, и он получает очень слабые результаты по сравнению с ИИ, которые ищут все возможности, поскольку он только смотрит на одну плиту впереди. Он был представлен на раннем этапе сроков ответа.

Я уточнил алгоритм и избил игру! Это может закончиться неудачей из-за простой неудачи близко к концу (вы вынуждены двигаться вниз, чего вам никогда не следует делать, и плитка появляется там, где должно быть самое высокое. Просто постарайтесь, чтобы верхняя строка была заполнена, поэтому перемещение влево не разбить шаблон), но в основном у вас есть фиксированная часть и мобильная часть для игры. Это ваша цель:

Ready to finish

Это модель, которую я выбрал по умолчанию.

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

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

Здесь идет алгоритм. Около 80% побед (кажется, всегда можно выиграть с более "профессиональными" методами ИИ, я не уверен в этом.)

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)
  }

Несколько указателей на недостающие шаги. Здесь: model change

Модель изменилась из-за удачи быть ближе к ожидаемой модели. Модель, которую ИИ пытается достичь, - это

 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), затем будет продолжать цепочку до тех пор, пока она не получит:

Chain completed

Итак, теперь модель и цепочка вернулись к:

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

Второй указатель, ему не повезло, и его основное место было принято. Вероятно, он потерпит неудачу, но он все равно может достичь этого:

Enter image description here

Здесь модель и цепочка:

  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
116
ответ дан Daren 12 марта '14 в 19:05
источник поделиться

Меня заинтересовала идея AI для этой игры, содержащая без жесткого кодирования (без эвристики, функции подсчета очков и т.д.). ИИ должен "знать" только правила игры и "определять" игру. Это контрастирует с большинством ИИ (например, в этой теме), где игра играет по существу грубую силу, управляемую функцией подсчета, представляющей человеческое понимание игры.

Алгоритм AI

Я нашел простой, но удивительно хороший игровой алгоритм. Чтобы определить следующий ход для данной доски, ИИ играет в игру до конца, используя случайные ходы. Это делается много раз, отслеживая конечные баллы. Затем вычисляется средний конечный балл за начальный ход. В качестве следующего шага выбирается начальный шаг с наивысшим средним баром.

Используя только 100 пробегов за ход, ИИ достигает 2048 плит в 80% случаев, а 4096 - в 50% случаев. Использование 10000 трасс получает 2048 плитку 100%, 70% для 4096 плиток и около 1% для плитки 8192.

Посмотрите в действии

Показатель наилучшего результата показан здесь:

best score

Интересный факт об этом алгоритме заключается в том, что, хотя игры с произвольным воспроизведением (неудивительно) довольно плохи, выбор лучшего (или наименее плохого) хода приводит к очень хорошей игре: типичная игра AI может достигать 70000 пунктов и в последний раз 3000 ходов, но случайное воспроизведение проходит из любой заданной позиции, дает в среднем 340 дополнительных очков и только 40 дополнительных ходов перед смертью. (Вы можете это увидеть сами, запустив ИИ и откройте консоль отладки.)

Этот график иллюстрирует этот момент: синяя линия показывает оценку доски после каждого хода. Красная строка показывает алгоритм лучший случайный балл с этой позиции. По сути, красные значения "тянут" синие значения вверх к ним, поскольку они являются наилучшим вариантом алгоритма. Интересно видеть, что красная линия находится всего лишь чуть выше синей линии в каждой точке, но синяя линия продолжает увеличиваться все больше и больше.

scoring graph

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

Поиск позже я нашел, что этот алгоритм можно классифицировать как алгоритм Pure Monte Carlo Tree Search.

Реализация и ссылки

Сначала я создал версию JavaScript, которая может выглядеть здесь. Эта версия может запустить 100 запусков в достойное время. Откройте консоль для дополнительной информации. (источник)

Позже, чтобы поиграть еще немного, я использовал @nneonneo высоко оптимизированную инфраструктуру и реализовал мою версию на С++. Эта версия позволяет до 100000 ходов за ход и даже 1000000, если у вас есть терпение. Указания по строительству. Он запускается в консоли и также имеет пульт дистанционного управления для воспроизведения веб-версии. (источник)

Результаты

Удивительно, но увеличение количества прогонов не приводит к существенному улучшению игры. Кажется, что предел этой стратегии составляет около 80000 пунктов с плитой 4096 и всеми меньшими, очень близкими к достижению плитки 8192. Увеличение количества прогонов от 100 до 100000 увеличивает коэффициенты для достижения этого предела оценки (от 5% до 40%), но не пробивает его.

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

Улучшения

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

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

Однако ни одна из этих идей не показала реального преимущества перед простой первой идеей. Я оставил код для этих идей, прокомментированных в коде на С++.

Я добавил механизм "Deep Search", который временно увеличил номер запуска до 1000000, когда любой из прогонов удалось случайно достичь следующей самой высокой плитки. Это позволило улучшить время.

Мне было бы интересно услышать, есть ли у кого-нибудь другие идеи усовершенствования, которые поддерживают доменную независимость AI.

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

Просто для удовольствия, я также реализовал AI как букмарклет, подключившись к игровым элементам управления. Это позволяет AI работать с оригинальной игрой и многими ее вариантами.

Это возможно из-за не зависящей от домена природы AI. Некоторые из вариантов весьма различны, например, шестиугольный клон.

110
ответ дан Ronenz 25 мая '14 в 12:25
источник поделиться

Я копирую здесь содержимое сообщения в моем блоге


Решение, которое я предлагаю, очень просто и легко реализовать. Хотя, он достиг отметки 131040. Представлены несколько эталонных характеристик алгоритма.

Score

Алгоритм

Эвристический алгоритм оценки

Предположение, на котором основан мой алгоритм, довольно прост: если вы хотите достичь более высокого балла, плата должна быть максимально аккуратной. В частности, оптимальная установка задается линейным и монотонным убывающим порядком значений плитки. Эта интуиция даст вам также верхнюю границу для значения плитки: s где n - количество плиток на доске.

(Там есть возможность добраться до плитки 131072, если 4-плитка генерируется случайным образом вместо 2-плитки, когда это необходимо)

Два возможных способа организации платы показаны на следующих изображениях:

enter image description here

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

s

s

Несколько линейных путей можно оценить сразу, окончательный счет будет максимальным счетом любого пути.

Правило принятия решений

Реализованное правило решения не очень умное, здесь представлен код в 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 наверняка улучшит алгоритм. Очевидно, что более сложное правило принятия решения замедлит алгоритм, и для его выполнения потребуется некоторое время. В ближайшем будущем я попытаюсь выполнить минимаксную реализацию. (следите за обновлениями)

Benchmark

  • T1 - 121 тест - 8 разных путей - r = 0.125
  • T2 - 122 теста - 8-разные пути - r = 0.25
  • T3 - 132 теста - 8-разные пути - r = 0.5
  • T4 - 211 тесты - 2-разные пути - r = 0.125
  • T5 - 274 теста - 2-разные пути - r = 0.25
  • T6 - 211 тест - 2-разные пути - r = 0,5

enter image description hereenter image description hereenter image description hereenter image description here

В случае T2 четыре теста из десяти генерируют плиту 4096 со средней оценкой s 42000

код

Код можно найти на GiHub по следующей ссылке: https://github.com/Nicola17/term2048-AI Он основан на term2048 и написан на Python. Я как можно скорее реализую более эффективную версию на С++.

86
ответ дан Nicola Pezzotti 27 марта '14 в 1:13
источник поделиться

Моя попытка использует expectimax как другие решения выше, но без битов. Решение 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]]

который заставляет упорядочивать плитки по убыванию в виде змеи из верхней левой плитки.

Код ниже или jsbin:

  
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)

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


hint.onclick = function() {
	hintvalue.innerHTML = ['up', 'right', 'down', 'left'][predict(ai)]
}
document.addEventListener("keydown", function(event) {
	if (event.which in map) {
		move(map[event.which], ai)
		console.log(stats(ai.grid))
		updateUI(ai)
	}
})
var map = {
	38: 0, // Up
	39: 1, // Right
	40: 2, // Down
	37: 3, // Left
};
init.onclick = function() {
	initialize(ai);
	updateUI(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 {
	text-align: center
}
table, th, td {
    border: 1px solid black;
    margin: 5px auto;
}
td {
    width: 35px;
    height: 35px;
    text-align: center;
}
<table></table>
<button id=init>init</button><button id=runai>run AI</button><button id=hint>hint</button><span id=hintvalue></span>
27
ответ дан caub 03 марта '15 в 8:35
источник поделиться

Я думаю, что нашел алгоритм, который работает достаточно хорошо, так как я часто достигаю десятков тысяч, а мое личное преимущество - около 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);
}
24
ответ дан Vincent Lecrubier 12 марта '14 в 21:57
источник поделиться

Я являюсь автором контроллера 2048, который лучше, чем любая другая программа, упомянутая в этом потоке. Эффективная реализация контроллера доступна на github. В отдельном репо существует также код, используемый для обучения функции оценки состояния контроллера. Метод обучения описан в документе .

Контроллер использует expectimax-поиск с функцией оценки состояния, полученной с нуля (без экспертизы человека 2048) по варианту временного разностного обучения (метод обучения усилению). Функция state-value использует сеть 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%

(Последняя строка означает наличие заданных фрагментов одновременно на плате).

Для 3-ply:

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

Однако я никогда не видел, чтобы он получал плиту 65536.

22
ответ дан cauchy 21 дек. '15 в 13:49
источник поделиться

В этой игре уже реализована реализация AI: здесь. Выдержка из README:

Алгоритм - это итеративный углубление глубины первого альфа-бета-поиска. Функция оценки пытается сохранить монотонность строк и столбцов (либо уменьшаясь, либо увеличиваясь), минимизируя количество плиток в сетке.

Существует также обсуждение ycombinator об этом алгоритме, который может оказаться полезным.

20
ответ дан baltazar 13 марта '14 в 12:16
источник поделиться

Алгоритм

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)

Это упрощенная проверка возможности слияния внутри этого состояния, не задумываясь.

Примечание: константы могут быть изменены..

19
ответ дан Khaled.K 12 марта '14 в 23:15
источник поделиться

Это не прямой ответ на вопрос OP, это больше из материалов (экспериментов), которые я до сих пор пытался решить одну и ту же проблему, и получил некоторые результаты и получил некоторые замечания, которые хочу поделиться, мне интересно, если мы может иметь некоторые дополнительные сведения из этого.

Я просто попытался выполнить минимаксную реализацию с альфа-бета-обрезкой с обрезкой глубины поиска в 3 и 5. Я пытался решить ту же проблему для сетки 4x4 как назначение проекта для edX курса ColumbiaX: CSMM.101x Искусственный интеллект (AI).

Я применил выпуклую комбинацию (пробовал различные эвристические веса) пары эвристических оценочных функций, в основном из интуиции и из тех, которые обсуждались выше:

  • Монотонность
  • Свободное пространство доступно

В моем случае компьютерный плейер полностью случайный, но все же я принял состязательные настройки и реализовал агент игрока AI в качестве максимального игрока.

У меня есть сетка 4x4 для игры.

Наблюдение:

Если я назначаю слишком много веса первой эвристической функции или второй эвристической функции, то оба случая, на которые попадает игрок AI, являются низкими. Я играл со многими возможными назначениями веса для эвристических функций и принимал выпуклую комбинацию, но очень редко AI-плеер мог забивать 2048. В большинстве случаев он либо останавливается на 1024, либо на 512.

Я также пробовал эвристику угла, но почему-то он делает результаты хуже, любой интуиции почему?

Кроме того, я попытался увеличить отключение глубины поиска с 3 до 5 (я не могу увеличить его больше, поскольку поиск этого пространства превышает допустимое время даже при обрезке) и добавил еще одну эвристику, которая смотрит на значения смежных плитки и дает больше очков, если они слияния, но все же я не могу получить 2048.

Я думаю, что лучше использовать Expectimax вместо минимаксного, но все же я хочу решить эту проблему только с минимаксным и получить высокие оценки, такие как 2048 или 4096. Я не уверен, что у меня что-то не хватает.

Ниже анимации показаны последние несколько шагов игры, которую играет агент AI с компьютерным проигрывателем:

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

Любая информация будет очень полезной, заблаговременно. (Это ссылка моего сообщения в блоге для статьи: https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve-2048-game-with-computer/)

Следующая анимация показывает последние несколько шагов игры, в которых агент AI-игрока может получить 2048 баллов, на этот раз добавив также эвристику абсолютного значения:

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

На следующих рисунках показано дерево игры, изученное агентом AI игрока, предполагая, что компьютер является противником всего на один шаг:

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

7
ответ дан Sandipan Dey 07 марта '17 в 0:37
источник поделиться

Я написал 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

6
ответ дан wvdz 04 апр. '14 в 3:49
источник поделиться

Этот алгоритм не является оптимальным для выигрыша игры, но он достаточно оптимален с точки зрения производительности и количества необходимого кода:

  if(can move neither right, up or down)
    direction = left
  else
  {
    do
    {
      direction = random from (right, down, up)
    }
    while(can not move in "direction")
  }
3
ответ дан API-Beast 15 марта '14 в 0:53
источник поделиться

Многие из других ответов используют ИИ с вычислительно дорогостоящим поиском возможных фьючерсов, эвристики, обучения и тому подобного. Это впечатляет и, вероятно, правильный путь вперед, но я хочу внести еще одну идею.

Моделируйте стратегию, которую используют хорошие игроки игры.

Например:

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

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

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


Плитка требует слияния с соседом, но слишком мала: слейте другого соседа с этим.

Более крупная плитка в пути: Увеличьте значение меньшего окружающего плитка.

и т.д...


Весь подход, вероятно, будет более сложным, чем это, но не намного сложнее. Это может быть эта механическая в ощущении нехватки баллов, весов, нейронов и глубоких поисков возможностей. Дерево возможностей rairly даже должно быть достаточно большим, чтобы потребоваться какое-либо разветвление вообще.

0
ответ дан alan2here 10 авг. '15 в 17:39
источник поделиться

Другие вопросы по меткам