Какой алгоритм игры в крестики-нолики я могу использовать, чтобы определить «лучший ход» для ИИ?

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

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

imgs.xkcd.com/comics/tic_tac_toe.png
Mooing Duck 21.06.2012 20:17

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

Ben Carp 06.01.2018 17:38

Я задавал себе что-то подобное: blog.maxant.co.uk/pebble/2018/04/07/1523086680000.html

Ant Kutschera 11.04.2018 21:53

Вот очень визуальный ответ.

rbinnun 02.11.2018 17:31
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
61
4
114 131
10

Ответы 10

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

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

djechlin 12.04.2016 20:15

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

Как только вы дойдете до этой точки, в древовидной диаграмме будет что-то вроде менее 1 КБ данных, чтобы описать результат, и, следовательно, лучший ход для компьютера.

-Адам

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

Daniel Spiewak 24.09.2008 09:32

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

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

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

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

djechlin 12.04.2016 20:14

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

Стратегия из Википедии для идеальной игры (каждый раз выигрыш или ничья) кажется простым псевдокодом:

Quote from Wikipedia (Tic Tac Toe#Strategy)

A player can play a perfect game of Tic-tac-toe (to win or, at least, draw) if they choose the first available move from the following list, each turn, as used in Newell and Simon's 1972 tic-tac-toe program.[6]

  1. Win: If you have two in a row, play the third to get three in a row.

  2. Block: If the opponent has two in a row, play the third to block them.

  3. Fork: Create an opportunity where you can win in two ways.

  4. Block Opponent's Fork:

    Option 1: Create two in a row to force the opponent into defending, as long as it doesn't result in them creating a fork or winning. For example, if "X" has a corner, "O" has the center, and "X" has the opposite corner as well, "O" must not play a corner in order to win. (Playing a corner in this scenario creates a fork for "X" to win.)

    Option 2: If there is a configuration where the opponent can fork, block that fork.

  5. Center: Play the center.

  6. Opposite Corner: If the opponent is in the corner, play the opposite corner.

  7. Empty Corner: Play an empty corner.

  8. Empty Side: Play an empty side.

Распознавание того, как выглядит ситуация «вилки», может быть выполнено методом грубой силы, как предлагается.

Примечание: «Идеальный» противник - хорошее упражнение, но в конечном итоге не стоит «играть» против него. Однако вы можете изменить вышеперечисленные приоритеты, чтобы дать характерные слабости личностям оппонента.

Как бы вы тогда посоветовали реализовать ответвления стратегии?

Andrew Szeto 07.04.2009 23:17

Итак, вы говорите: единственный выигрышный ход - не играть.

tooleb 15.06.2009 23:16

Разве центральная вилка не была бы более ценной, чем другие вилки?

Daniel Kobe 23.09.2015 16:41

@Nick "попробуй себя" здесь немного бесполезен без какой-либо информации о том, как найти контрпример. Существует ли конфигурация, достижимая с помощью этой стратегии, при которой следование (6) вместо (7), например, приведет к проигрышной игре? Было бы полезно разместить дополнительную информацию о вашем контрпримере.

djechlin 12.04.2016 20:11

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

Вкратце, вы хотите не искать ход, который имеет для вас наилучший из возможных исходов, а скорее искать ход, при котором наихудший из возможных исходов максимально хорош. Если вы предполагаете, что ваш оппонент играет оптимально, вы должны предположить, что он сделает худший для вас ход, и поэтому вы должны сделать ход, который минимизирует их МАКСИМАЛЬНЫЙ выигрыш.

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

guidot 06.07.2012 17:54

Область поиска @guidot Tic-tac-toe настолько мала, что ваша функция оценки тривиальна: + inf, если игра является выигрышным состоянием, -inf, если это проигрышное состояние, 0, если это ничья.

Nick Johnson 08.07.2012 06:04

Минимакс или альфа-бета - это, конечно, не первая идея, которую нужно преследовать для такой соперничающей игры (это ограничивает ценность исходного ответа). Однако если вы это сделаете (возможно, с целью перейти к более сложным играм, таким как го-моку), вам понадобится функция оценки. Такая функция имеет смысл только для данных алгоритмов, если она дает результат для ЛЮБОЙ промежуточной позиции, предлагаемая (очень общая), которая ограничена завершенными играми, полезна только для выбора окончательного сообщения о выигрыше.

guidot 08.07.2012 12:56

Напротив, минимакс или альфа-бета с функцией оценки «все или ничего» применимы к любой игре, в которой вы хотите провести тщательный поиск. Альфа-бета существенно сокращает пространство поиска по сравнению с перебором; Minimax - это просто разумный способ выполнить поиск в дереве игры и найти лучший доступный ход.

Nick Johnson 09.07.2012 06:40

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

guidot 09.07.2012 11:25

Я не говорил, что вы всегда можете выполнять исчерпывающий поиск, просто когда вы можете - например, в Tic Tac Toe - все, что вам нужно, - это оценщик по принципу «все или ничего».

Nick Johnson 09.07.2012 13:42

Возникает вопрос. Если реализация минимакса требует того факта, что пространство поиска невелико, просто решите проблему прямым перебором.

djechlin 12.04.2016 20:12

Попытка без использования игрового поля.

  1. чтобы выиграть (ваш дубль)
  2. в противном случае не проиграть (дубль соперника)
  3. если нет, у вас уже есть вилка (есть дабл дабл)
  4. если нет, то если у соперника есть вилка
    1. поиск в блокирующих точках возможного дабла и форка (окончательный выигрыш)
    2. если не искать вилки в блокировочных точках (что дает противнику наиболее проигрышные возможности)
    3. если бы не только блокирующие баллы (чтобы не потерять)
  5. если не искать дабл и форк (окончательная победа)
  6. если не искать только вилки, которые дают противнику самые проигрышные возможности
  7. если не искать только двойного
  8. если не тупик, то галстук, рандом.
  9. если нет (это означает ваш первый ход)
    1. если это первый ход игры;
      1. дать противнику наибольшую вероятность проигрыша (алгоритм дает только угловые, что дает противнику 7 проигрышных очков)
      2. или для того, чтобы разбить скуку просто случайным образом.
    2. если это второй ход игры;
      1. найти только не теряющие очки (дает еще немного вариантов)
      2. или найдите в этом списке те точки, которые имеют наибольшие шансы на победу (это может быть скучно, потому что результатом будут только все углы или смежные углы или центр)

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

На самом деле я имел в виду попытку без использования дерева игр, которое является оптимальным решением для такого рода задач. Только для того, чтобы надеяться на более глубокое понимание.

Mesut Ergul 12.08.2012 15:09

Типичный алгоритм игры в крестики-нолики должен выглядеть так:

Доска: вектор из девяти элементов, представляющий доску. Мы храним 2 (с указанием Пробел), 3 (обозначает X) или 5 (обозначает O). Ход: целое число, указывающее, какой ход игры будет сделан. Первый ход будет обозначен цифрой 1, последний - 9.

Алгоритм

Основной алгоритм использует три функции.

Make2: возвращает 5, если центральный квадрат платы пуст, т.е. если board[5]=2. В противном случае эта функция возвращает любой неугловой квадрат (2, 4, 6 or 8).

Posswin(p): возвращает 0, если игрок p не может выиграть следующим ходом; в противном случае он возвращает номер клетки, составляющей выигрышный ход. Эта функция позволит программе как побеждать, так и блокировать победу оппонентов. Эта функция работает, проверяя каждую из строк, столбцов и диагоналей. Умножая значения каждого квадрата на всю строку (столбец или диагональ), можно проверить возможность выигрыша. Если продукт 18 (3 x 3 x 2), то X может выиграть. Если продукт 50 (5 x 5 x 2), то O может выиграть. Если выигрышная строка (столбец или диагональ) найдена, можно определить пустой квадрат в ней, и эта функция вернет номер этого квадрата.

Go (n): делает ход в квадрате n. эта процедура устанавливает для платы [n] значение 3, если Терн нечетный, или 5, если Терн четный. Он также увеличивает ход на единицу.

Алгоритм имеет встроенную стратегию для каждого хода. Это делает нечетные номера ход, если он играет X, четный ход, если он играет O.

Turn = 1    Go(1)   (upper left corner).
Turn = 2    If Board[5] is blank, Go(5), else Go(1).
Turn = 3    If Board[9] is blank, Go(9), else Go(3).
Turn = 4    If Posswin(X) is not 0, then Go(Posswin(X)) i.e. [ block opponent’s win], else Go(Make2).
Turn = 5    if Posswin(X) is not 0 then Go(Posswin(X)) [i.e. win], else if Posswin(O) is not 0, then Go(Posswin(O)) [i.e. block win], else if Board[7] is blank, then Go(7), else Go(3). [to explore other possibility if there be any ].
Turn = 6    If Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else Go(Make2).
Turn = 7    If Posswin(X) is not 0 then Go(Posswin(X)), else if Posswin(X) is not 0, then Go(Posswin(O)) else go anywhere that is blank.
Turn = 8    if Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else go anywhere that is blank.
Turn = 9    Same as Turn=7.

Я использовал это. Дайте мне знать, что вы чувствуете.

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

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

Если P2 делает правильно блокирует в центре, P1 должен играть в противоположном углу, потому что опять же, по какой-то психологической причине, P2 предпочтет симметрию игры в углу, что снова приведет к проигрышной доске для них.

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

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

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

Я знаю, что мне очень весело на вечеринках.

Я тоже развлекаюсь на вечеринках - мы должны собираться вместе ... И поэтому я не согласен с вашим утверждением, что игра P1 в углу дает выигрыши гораздо чаще. У вас есть ссылка на это? Мой анализ показывает, что центр лучше, хотя это зависит от типа игрока: blog.maxant.co.uk/pebble/2018/04/07/1523086680000.html

Ant Kutschera 07.04.2018 23:46

@AntKutschera без упоминаний, только личный опыт, но я чувствовал себя уверенно, потому что психология / интуиция настолько сильны, что неортодоксальные открытия требуют нестандартных ответов. Если по какой-то другой причине у игрока есть предварительные предположения или он настроен иначе, я думаю, это не сработает.

djechlin 09.04.2018 19:23

Адаптация крестиков-ноликов к алгоритму min max

let gameBoard: [
    [null, null, null],
    [null, null, null],
    [null, null, null]
]

const SYMBOLS = {
    X:'X',
    O:'O'
}

const RESULT = {
    INCOMPLETE: "incomplete",
    PLAYER_X_WON: SYMBOLS.x,
    PLAYER_O_WON: SYMBOLS.o,
    tie: "tie"
}

Нам понадобится функция, которая может проверять результат. Функция проверит последовательность символов. Каким бы ни было состояние доски, результатом может быть один из 4 вариантов: либо «Неполно», игрок X выиграл, игрок O выиграл или ничья.

function checkSuccession (line){
    if (line === SYMBOLS.X.repeat(3)) return SYMBOLS.X
    if (line === SYMBOLS.O.repeat(3)) return SYMBOLS.O
    return false 
}

function getResult(board){

    let result = RESULT.incomplete
    if (moveCount(board)<5){
        return result
    }

    let lines

    //first we check row, then column, then diagonal
    for (var i = 0 ; i<3 ; i++){
        lines.push(board[i].join(''))
    }

    for (var j=0 ; j<3; j++){
        const column = [board[0][j],board[1][j],board[2][j]]
        lines.push(column.join(''))
    }

    const diag1 = [board[0][0],board[1][1],board[2][2]]
    lines.push(diag1.join(''))
    const diag2 = [board[0][2],board[1][1],board[2][0]]
    lines.push(diag2.join(''))
    
    for (i=0 ; i<lines.length ; i++){
        const succession = checkSuccesion(lines[i])
        if (succession){
            return succession
        }
    }

    //Check for tie
    if (moveCount(board)==9){
        return RESULT.tie
    }

    return result
}

Наша функция getBestMove получит состояние доски и символ игрока, для которого мы хотим определить наилучший ход. Наша функция будет проверять все возможные ходы с помощью функции getResult. Если это победа, она получит оценку 1. если проиграет, она получит оценку -1, ничья получит оценку 0. Если это не определено, мы вызовем функцию getBestMove с новым состоянием. доски и противоположный символ. Поскольку следующий ход принадлежит оппоненту, его победой считается проигрыш текущего игрока, и счет аннулируется. В конце возможный ход получает оценку 1,0 или -1, мы можем отсортировать ходы и вернуть ход с наибольшим количеством очков.

const copyBoard = (board) => board.map( 
    row => row.map( square => square  ) 
)

function getAvailableMoves (board) {
  let availableMoves = []
  for (let row = 0 ; row<3 ; row++){
    for (let column = 0 ; column<3 ; column++){
      if (board[row][column]===null){
        availableMoves.push({row, column})
      }
    }
  }
  return availableMoves
}

function applyMove(board,move, symbol) {
  board[move.row][move.column]= symbol
  return board
}
 
function getBestMove (board, symbol){

    let availableMoves = getAvailableMoves(board)

    let availableMovesAndScores = []

    for (var i=0 ; i<availableMoves.length ; i++){
      let move = availableMoves[i]
      let newBoard = copyBoard(board)
      newBoard = applyMove(newBoard,move, symbol)
      result = getResult(newBoard,symbol).result
      let score
      if (result == RESULT.tie) {score = 0}
      else if (result == symbol) {
        score = 1
      }
      else {
        let otherSymbol = (symbol==SYMBOLS.x)? SYMBOLS.o : SYMBOLS.x
        nextMove = getBestMove(newBoard, otherSymbol)
        score = - (nextMove.score)
      }
      if (score === 1)  // Performance optimization
        return {move, score}
      availableMovesAndScores.push({move, score})
    }

    availableMovesAndScores.sort((moveA, moveB )=>{
        return moveB.score - moveA.score
      })
    return availableMovesAndScores[0]
  }

Алгоритм в действии, Github, Объясняя процесс более подробно

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