Крестики-нолики с минимаксом: компьютер иногда проигрывает, когда игрок идет первым; работает иначе

Я работаю над алгоритмом Minimax для создания непревзойденных Tic Tac Toe. Мне нужно, чтобы он работал как при первом включении компьютера, так и при первом включении плеера. В текущей версии компьютер никогда не проиграет при первом запуске. Однако кажется, что Minimax никогда не находит лучший ход (всегда возвращает -1 в качестве счета), если игрок ходит первым.

Что приводит к тому, что минимаксный счет возвращается к -1 для компьютера, если игрок делает первый ход?

Пример:

board.addMark( 1, Mark2.PLAYER ); // add a 'PLAYER' mark to an arbitrary location
Minimax.minimax( board, Mark2.COMPUTER ); // will always return -1

Вот класс Minimax:

public class Minimax {
  public static int minimax( Board board, Mark2 currentMark ) {
    int score = (currentMark == Mark2.COMPUTER) ? -1 : 1;
    int[] availableSpaces = board.getAvailableSpaces();

    if ( board.hasWinningSolution() )
      score = (board.getWinningMark() == Mark2.COMPUTER) ? 1 : -1;
    else if ( availableSpaces.length != 0 ) {
      Mark2 nextMark = (currentMark == Mark2.COMPUTER) ? Mark2.PLAYER : Mark2.COMPUTER;

      for ( int availableIndex = 0; availableIndex < availableSpaces.length; availableIndex++ ) {
        board.addMark( availableSpaces[availableIndex], currentMark );
        int nextScore = minimax( board, nextMark );
        board.eraseMark( availableSpaces[availableIndex] );

        if ( currentMark == Mark2.COMPUTER && nextScore > score
            || currentMark == Mark2.PLAYER && nextScore < score )
          score = nextScore;
      }
    }

    return score;
  }
}

Вот класс Board:

public class Board {
  private Mark2 gameBoard[];
  private int blankSpaces;

  private boolean solutionFound;
  private Mark2 winningMark;

  public final static int winSets[][] = {
      { 0, 1, 2 }, { 3, 4, 5 },
      { 6, 7, 8 }, { 0, 3, 6 },
      { 1, 4, 7 }, { 2, 5, 8 },
      { 0, 4, 8 }, { 2, 4, 6 }
  };

  public Board() {
    gameBoard = new Mark2[9];
    blankSpaces = 9;

    for ( int boardIndex = 0; boardIndex < gameBoard.length; boardIndex++ ) {
      gameBoard[boardIndex] = Mark2.BLANK;
    }
  }

  public void addMark( int spaceIndex, Mark2 mark ) {
    if ( gameBoard[spaceIndex] != mark ) {
      gameBoard[spaceIndex] = mark;

      if ( mark == Mark2.BLANK ) {
        blankSpaces++;
      } else {
        blankSpaces--;
      }
    }
  }

  public void eraseMark( int spaceIndex ) {
    if ( gameBoard[spaceIndex] != Mark2.BLANK ) {
      gameBoard[spaceIndex] = Mark2.BLANK;
      blankSpaces++;
    }
  }

  public int[] getAvailableSpaces() {
    int spaces[] = new int[blankSpaces];
    int spacesIndex = 0;

    for ( int boardIndex = 0; boardIndex < gameBoard.length; boardIndex++ )
      if ( gameBoard[boardIndex] == Mark2.BLANK )
        spaces[spacesIndex++] = boardIndex;

    return spaces;
  }

  public Mark2 getMarkAtIndex( int spaceIndex ) {
    return gameBoard[spaceIndex];
  }

  public boolean hasWinningSolution() {
    this.solutionFound = false;
    this.winningMark = Mark2.BLANK;

    for ( int setIndex = 0; setIndex < winSets.length && !solutionFound; setIndex++ )
      checkSpacesForWinningSolution( winSets[setIndex][0], winSets[setIndex][1], winSets[setIndex][2] );

    return solutionFound;
  }

  public Mark2 getWinningMark() {
    return this.winningMark;
  }

  private void checkSpacesForWinningSolution( int first, int second, int third ) {
    if ( gameBoard[first] == gameBoard[second] && gameBoard[third] == gameBoard[first]
        && gameBoard[first] != Mark2.BLANK ) {
      solutionFound = true;
      winningMark = gameBoard[first];
    }
  }

  public void printBoard() {
    System.out.printf( " %c | %c | %c\n", getMarkCharacter( gameBoard[0] ), getMarkCharacter( gameBoard[1] ),
        getMarkCharacter( gameBoard[2] ) );
    System.out.println( "------------" );
    System.out.printf( " %c | %c | %c\n", getMarkCharacter( gameBoard[3] ), getMarkCharacter( gameBoard[4] ),
        getMarkCharacter( gameBoard[5] ) );
    System.out.println( "------------" );
    System.out.printf( " %c | %c | %c\n", getMarkCharacter( gameBoard[6] ), getMarkCharacter( gameBoard[7] ),
        getMarkCharacter( gameBoard[8] ) );
  }

  public char getMarkCharacter( Mark2 mark ) {
    char result = (mark == Mark2.PLAYER) ? 'O' : ' ';
    result = (mark == Mark2.COMPUTER) ? 'X' : result;
    return result;
  }
}

А вот класс Mark2, если возникла путаница:

public enum Mark2 {
  BLANK,
  PLAYER,
  COMPUTER
}

person jneander    schedule 21.05.2012    source источник
comment
Хороший вопрос, но для более быстрой помощи (добавьте эти источники в один файл, добавьте main &) опубликуйте SSCCE .   -  person Andrew Thompson    schedule 21.05.2012
comment
непревзойденные крестики-нолики. Вы когда-нибудь думали об использовании своих способностей ... для добра? Какое развлечение - это непревзойденные крестики-нолики? ;)   -  person Andrew Thompson    schedule 21.05.2012
comment
@AndrewThompson Конечно же, самое интересное в том, чтобы заставить его работать должным образом.   -  person jneander    schedule 21.05.2012


Ответы (3)


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

Изначально я изменил инициализацию «оценки» на самые низкие / самые высокие возможные результаты (-1/1) и сравнил их с ними. Однако это запрещало результату получать наименьшее / наибольшее значение исключительно из набора возвращенных оценок, а вместо этого также включало инициализированное значение. Это испортило результаты.

Я добавил следующее сравнение к условному изменению «оценки»:

|| availableIndex == 0

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

public class Minimax {
  public static int minimax( Board board, Mark2 currentMark ) {
    int score = 0;
    int[] availableSpaces = board.getAvailableSpaces();

    if ( board.hasWinningSolution() )
      score = (board.getWinningMark() == Mark2.COMPUTER) ? 1 : -1;
    else if ( availableSpaces.length != 0 ) {
      Mark2 nextMark = (currentMark == Mark2.COMPUTER) ? Mark2.PLAYER : Mark2.COMPUTER;

      for ( int availableIndex = 0; availableIndex < availableSpaces.length; availableIndex++ ) {
        board.addMark( availableSpaces[availableIndex], currentMark );
        int nextScore = minimax( board, nextMark );
        board.eraseMark( availableSpaces[availableIndex] );

        if ( currentMark == Mark2.COMPUTER && nextScore > score
            || currentMark == Mark2.PLAYER && nextScore < score
            || availableIndex == 0 )
          score = nextScore;
      }
    }

    return score;
  }
}
person jneander    schedule 21.05.2012

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

Теперь компьютер запускается, оценка = -1. Нет выигрышного решения (проверяется один выигрышный набор, это не 1-в-ряд), и есть доступное место. Итак, мы продолжаем откатом, чтобы попробовать одну доступную позицию.

Теперь Отметьте = ИГРОК, и на доске есть выигрышное решение. Итак, компьютер побеждает, оценка = -1.

Возвращаясь к первому вызову, строка «int nextScore = minimax (board, nextMark);» снова возвращает -1, и окончательный результат равен -1.

То же самое происходит с вами и с большой проблемой.

person Guy Adini    schedule 21.05.2012

В игре «Крестики-нолики» есть 3 возможности, а не только 2: Побеждает Игрок1, Побеждает Игрок2, Никто не побеждает.

Вам следует заменить строки, подобные этой:

int score = (currentMark == Mark2.COMPUTER) ? -1 : 1;

примерно так:

int score = (currentMark == Mark2.COMPUTER) ? -1 : ((currentMark == Mark2.PLAYER) ? 1 : 0);
person Thomash    schedule 21.05.2012