Как заставить мой алгоритм ИИ играть в крестики-нолики на 9 досках? - PullRequest
4 голосов
/ 24 апреля 2019

Для того, чтобы другим было легко помочь мне Я поместил здесь все коды https://pastebin.com/WENzM41k , он начнется, когда 2 агента соревнуются друг с другом.

Я пытаюсь реализовать поиск по дереву Монте-Карло, чтобы играть в крестики-нолики с 9 досками в Python.Правила игры похожи на обычные крестики-нолики, но с 9 подплатами 3х3.Место последней фигуры решает, на какой вспомогательной доске разместить вашу фигуру.Это похоже на окончательный крестики-нолики, но если один из вспомогательных щитов выигран, игра заканчивается.

Я пытаюсь выучить MCTS и нашел здесь некоторый код: http://mcts.ai/code/python.html

Я использовал класс узла и класс UCT на веб-сайте и добавил свои 9-ти платные крестики-нолики.игровой класс состояния и некоторые другие коды.Все коды здесь:

from math import log, sqrt
import random
import numpy as np
from copy import deepcopy


class BigGameState:
    def __init__(self):
        self.board = np.zeros((10, 10), dtype="int8")
        self.curr = 1
        self.playerJustMoved = 2 # At the root pretend the player just moved is player 2 - player 1 has the first move

    def Clone(self):
        """ Create a deep clone of this game state.
        """
        st = BigGameState()
        st.playerJustMoved = self.playerJustMoved
        st.curr = self.curr
        st.board = deepcopy(self.board)
        return st

    def DoMove(self, move):
        """ Update a state by carrying out the given move.
            Must update playerJustMoved.
        """
        self.playerJustMoved = 3 - self.playerJustMoved
        if move >= 1 and move <= 9 and move == int(move) and self.board[self.curr][move] == 0:
            self.board[self.curr][move] = self.playerJustMoved
            self.curr = move

    def GetMoves(self):
        """ Get all possible moves from this state.
        """
        return [i for i in range(1, 10) if self.board[self.curr][i] == 0]

    def GetResult(self, playerjm):
        """ Get the game result from the viewpoint of playerjm. 
        """
        for bo in self.board:
            for (x,y,z) in [(1,2,3),(4,5,6),(7,8,9),(1,4,7),(2,5,8),(3,6,9),(1,5,9),(3,5,7)]:
                if bo[x] == [y] == bo[z]:
                    if bo[x] == playerjm:
                        return 1.0
                    else:
                        return 0.0
        if self.GetMoves() == []: return 0.5 # draw

    def drawboard(self):
        print_board_row(self.board, 1, 2, 3, 1, 2, 3)
        print_board_row(self.board, 1, 2, 3, 4, 5, 6)
        print_board_row(self.board, 1, 2, 3, 7, 8, 9)
        print(" ------+-------+------")
        print_board_row(self.board, 4, 5, 6, 1, 2, 3)
        print_board_row(self.board, 4, 5, 6, 4, 5, 6)
        print_board_row(self.board, 4, 5, 6, 7, 8, 9)
        print(" ------+-------+------")
        print_board_row(self.board, 7, 8, 9, 1, 2, 3)
        print_board_row(self.board, 7, 8, 9, 4, 5, 6)
        print_board_row(self.board, 7, 8, 9, 7, 8, 9)
        print()


def print_board_row(board, a, b, c, i, j, k):
    # The marking script doesn't seem to like this either, so just take it out to submit
    print("", board[a][i], board[a][j], board[a][k], end = " | ")
    print(board[b][i], board[b][j], board[b][k], end = " | ")
    print(board[c][i], board[c][j], board[c][k])


class Node:
    """ A node in the game tree. Note wins is always from the viewpoint of playerJustMoved.
        Crashes if state not specified.
    """
    def __init__(self, move = None, parent = None, state = None):
        self.move = move # the move that got us to this node - "None" for the root node
        self.parentNode = parent # "None" for the root node
        self.childNodes = []
        self.wins = 0
        self.visits = 0
        self.untriedMoves = state.GetMoves() # future child nodes
        self.playerJustMoved = state.playerJustMoved # the only part of the state that the Node needs later


    def UCTSelectChild(self):
        """ Use the UCB1 formula to select a child node. Often a constant UCTK is applied so we have
            lambda c: c.wins/c.visits + UCTK * sqrt(2*log(self.visits)/c.visits to vary the amount of
            exploration versus exploitation.
        """
        s = sorted(self.childNodes, key = lambda c: c.wins/c.visits + 0.2 * sqrt(2*log(self.visits)/c.visits))[-1]
        return s

    def AddChild(self, m, s):
        """ Remove m from untriedMoves and add a new child node for this move.
            Return the added child node
        """
        n = Node(move = m, parent = self, state = s)
        self.untriedMoves.remove(m)
        self.childNodes.append(n)
        return n

    def Update(self, result):
        """ Update this node - one additional visit and result additional wins. result must be from the viewpoint of playerJustmoved.
        """
        self.visits += 1
        self.wins += result

    def __repr__(self):
        return "[M:" + str(self.move) + " W/V:" + str(self.wins) + "/" + str(self.visits) + " U:" + str(self.untriedMoves) + "]"

    def TreeToString(self, indent):
        s = self.IndentString(indent) + str(self)
        for c in self.childNodes:
             s += c.TreeToString(indent+1)
        return s

    def IndentString(self,indent):
        s = "\n"
        for i in range (1,indent+1):
            s += "| "
        return s

    def ChildrenToString(self):
        s = ""
        for c in self.childNodes:
             s += str(c) + "\n"
        return s


def UCT(rootstate, itermax, verbose = False):
    """ Conduct a UCT search for itermax iterations starting from rootstate.
        Return the best move from the rootstate.
        Assumes 2 alternating players (player 1 starts), with game results in the range [0.0, 1.0]."""

    rootnode = Node(state = rootstate)

    for i in range(itermax):
        node = rootnode
        state = rootstate.Clone()

        # Select
        while node.untriedMoves == [] and node.childNodes != []: # node is fully expanded and non-terminal
            node = node.UCTSelectChild()
            state.DoMove(node.move)

        # Expand
        if node.untriedMoves != []: # if we can expand (i.e. state/node is non-terminal)
            m = random.choice(node.untriedMoves) 
            state.DoMove(m)
            node = node.AddChild(m,state) # add child and descend tree

        # Rollout - this can often be made orders of magnitude quicker using a state.GetRandomMove() function
        while state.GetMoves() != []: # while state is non-terminal
            state.DoMove(random.choice(state.GetMoves()))

        # Backpropagate
        while node != None: # backpropagate from the expanded node and work back to the root node
            node.Update(state.GetResult(node.playerJustMoved)) # state is terminal. Update node with result from POV of node.playerJustMoved
            node = node.parentNode

    # Output some information about the tree - can be omitted
    if (verbose): print(rootnode.TreeToString(0))
    else: print(rootnode.ChildrenToString())

    return sorted(rootnode.childNodes, key = lambda c: c.visits)[-1].move # return the move that was most visited

def UCTPlayGame():
    """ Play a sample game between two UCT players where each player gets a different number 
        of UCT iterations (= simulations = tree nodes).
    """

    state = BigGameState() # uncomment to play OXO

    while (state.GetMoves() != []):
        state.drawboard()
        m = UCT(rootstate = state, itermax = 1000, verbose = False) # play with values for itermax and verbose = True
        print("Best Move: " + str(m) + "\n")
        state.DoMove(m)
    if state.GetResult(state.playerJustMoved) == 1.0:
        print("Player " + str(state.playerJustMoved) + " wins!")
    elif state.GetResult(state.playerJustMoved) == 0.0:
        print("Player " + str(3 - state.playerJustMoved) + " wins!")
    else: print("Nobody wins!")

if __name__ == "__main__":
    """ Play a single game to the end using UCT for both players. 
    """
    UCTPlayGame()

Запустите код, который будет запущен, когда 2 агента конкурируют друг с другом.Однако агент не может хорошо играть в игру.Плохая игра не приемлема.Например, если ai получил 2 в ряду на дополнительной доске, и это его ход снова, он не разыгрывает выигрышный ход.С чего мне начать улучшаться и как?Я попытался изменить код класса Node и класса UCT, но ничего не получилось.

Обновление: если доска находится в состоянии ниже, и мой AI (играющий в Х) поворачивается, чтобы играть на доске 8 (средняя подпрограмма).доска третьего ряда).Должен ли я написать конкретный код, который позволил бы ИИ не играть на 1 или 5 (потому что тогда противник победит), или я должен позволить ИИ решать.Я пытался написать код, чтобы сообщить AI, но таким образом я должен пройти через все вспомогательные платы.

--O|---|---
-O-|---|---
---|---|---
-----------
---|-O-|---
---|-O-|---
---|---|---
-----------
---|---|---
---|---|---
---|---|---

Ответы [ 2 ]

3 голосов
/ 25 апреля 2019

Я потратил некоторое время, чтобы прочитать о MCTS, и еще больше времени, чтобы поймать остальные ошибки:

  1. Я добавил OXOState (tic-tac-toe), чтобы я мог отлаживать с помощью знакомых ипростая играЭто была только одна проблема с исходным исходным кодом http://mcts.ai/code/python.html:, он продолжит играть после того, как кто-то выиграет игру.Итак, я исправил это.
  2. Для отладки и веселья добавлен HumanPlayer.
  3. Для оценки уровня игры добавлены RandomPlayer и NegamaxPlayer (алгоритм negamax https://en.wikipedia.org/wiki/Negamax)

NegamaxPlayer против UCT (поиск по дереву Монте-Карло)

itermax= won lost draw total_time
       1 964   0   36       172.8
      10 923   0   77       173.4
     100 577   0  423       182.1
    1000  48   0  952       328.9
   10000   0   0 1000      1950.3

UTC играет весьма впечатляюще против идеального игрока (минимакс выполняет три поиска): при itermax = 1000 счет и время для игры почти равны- только 48 проигранных игр из 1000.

Исправлены остальные (я думаю) проблемы с BigGameState.Теперь он играет очень умело, поэтому я не могу выиграть.

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

NegamaxPlayer (глубина) против UCT (itermax)

depth itermax won  lost draw total_time
    4       1   9     1    0       18.4
    4      10   9     1    0       20.7
    4     100   5     5    0       36.2
    4    1000   2     8    0      188.8
    5   10000   2     8    0      318.0
    6   10000   0    10    0      996.5

Теперь UTC (itermax = 100) воспроизводит тот же уровень, что и NegamaxPlayer (глубина 4), и побеждает на следующем уровне 8-2.Я поражен!; -)

Я выиграл первую игру, в которую когда-либо играл на уровне (itermax = 100), но проиграл вторую игру на уровне 1000:

Game 1, Move 40:
┏━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ X X . ┃*O O O ┃ O . . ┃
┃ . O O ┃ . . X ┃ . X O ┃
┃ O X X ┃ X . . ┃ . X . ┃
┣━━━━━━━╋━━━━━━━╋━━━━━━━┫
┃ X . . ┃ . X . ┃ O . . ┃
┃ . X . ┃ O O X ┃ O X . ┃
┃ . O . ┃ O . . ┃ X . . ┃
┣━━━━━━━╋━━━━━━━╋━━━━━━━┫
┃ X X O ┃ O . X ┃ . O X ┃
┃ X . . ┃ . . . ┃ . . . ┃
┃ . . O ┃ O . X ┃ . O . ┃
┗━━━━━━━┻━━━━━━━┻━━━━━━━┛

Player 2 wins!
won 0 lost 1 draw 0

Вот полный код:

from math import *
import random
import time
from copy import deepcopy


class BigGameState:
    def __init__(self):
        self.board = [[0 for i in range(10)] for j in range(10)]
        self.curr = 1
        # At the root pretend the player just moved is player 2,
        # so player 1 will have the first move
        self.playerJustMoved = 2
        self.ended = False
        # to put * in __str__
        self.last_move = None
        self.last_curr = None

    def Clone(self):
        return deepcopy(self)

    def DoMove(self, move):
        # 1 2 3
        # 4 5 6
        # 7 8 9
        winning_pairs = [[],  # 0
                         [[2, 3], [5, 9], [4, 7]],  # for 1
                         [[1, 3], [5, 8]],  # for 2
                         [[1, 2], [5, 7], [6, 9]],  # for 3
                         [[1, 7], [5, 6]],  # for 4
                         [[1, 9], [2, 8], [3, 7], [4, 6]],  # for 5
                         [[3, 9], [4, 5]],  # for 6
                         [[1, 4], [5, 3], [8, 9]],  # for 7
                         [[7, 9], [2, 5]],  # for 8
                         [[7, 8], [1, 5], [3, 6]],  # for 9
                         ]
        if not isinstance(move, int) or 1 < move > 9 or \
                self.board[self.curr][move] != 0:
            raise ValueError
        self.playerJustMoved = 3 - self.playerJustMoved
        self.board[self.curr][move] = self.playerJustMoved
        for index1, index2 in winning_pairs[move]:
            if self.playerJustMoved == self.board[self.curr][index1] == \
                    self.board[self.curr][index2]:
                self.ended = True
        self.last_move = move
        self.last_curr = self.curr
        self.curr = move

    def GetMoves(self):
        if self.ended:
            return []
        return [i for i in range(1, 10) if self.board[self.curr][i] == 0]

    def GetResult(self, playerjm):
        # Get the game result from the viewpoint of playerjm.
        for bo in self.board:
            for x, y, z in [(1, 2, 3), (4, 5, 6), (7, 8, 9),
                            (1, 4, 7), (2, 5, 8), (3, 6, 9),
                            (1, 5, 9), (3, 5, 7)]:
                if bo[x] == bo[y] == bo[z]:
                    if bo[x] == playerjm:
                        return 1.0
                    elif bo[x] != 0:
                        return 0.0
        if not self.GetMoves():
            return 0.5 # draw
        raise ValueError

    def _one_board_string(self, a, row):
        return ''.join([' ' + '.XO'[self.board[a][i+row]] for i in range(3)])

    def _three_board_line(self, index, row):
        return '┃' + ''.join([self._one_board_string(i + index, row) + ' ┃' for i in range(3)])

    def __repr__(self):
        # ┏━━━━━━━┳━━━━━━━┳━━━━━━━┓
        # ┃ . . . ┃ . . . ┃ . . . ┃
        # ┃ . . . ┃ X . X ┃ . . O ┃
        # ┃ . X . ┃ . . O ┃ . . . ┃
        # ┣━━━━━━━╋━━━━━━━╋━━━━━━━┫
        # ┃ . . . ┃ . . . ┃*X X X ┃
        # ┃ X . O ┃ . . . ┃ O . O ┃
        # ┃ . . O ┃ . . . ┃ . . . ┃
        # ┣━━━━━━━╋━━━━━━━╋━━━━━━━┫
        # ┃ . . . ┃ . O . ┃ . O . ┃
        # ┃ . . . ┃ . . . ┃ . . X ┃
        # ┃ . . . ┃ . . . ┃ . . X ┃
        # ┗━━━━━━━┻━━━━━━━┻━━━━━━━┛
        s = '┏━━━━━━━┳━━━━━━━┳━━━━━━━┓\n'
        for i in [1, 4, 7]:
            for j in [1, 4, 7]:
                s += self._three_board_line(i, j) + '\n'
            if i != 7:
                s += '┣━━━━━━━╋━━━━━━━╋━━━━━━━┫\n'
            else:
                s += '┗━━━━━━━┻━━━━━━━┻━━━━━━━┛\n'
        # Hack: by rows and colums of move and current board
        # calculate place to put '*' so it is visible
        c = self.last_curr - 1
        c_c = c % 3
        c_r = c // 3
        m_c = (self.last_move - 1) % 3
        m_r = (self.last_move - 1)// 3
        p = 26 + c_r * (26 * 4) + c_c * 8 + m_r * 26 + m_c * 2 + 1
        s = s[:p] + '*' + s[p+1:]
        return s


class OXOState:
    def __init__(self):
        self.playerJustMoved = 2
        self.ended = False
        self.board = [0, 0, 0, 0, 0, 0, 0, 0, 0]

    def Clone(self):
        return deepcopy(self)

    def DoMove(self, move):
        #  0 1 2
        #  3 4 5
        #  6 7 8
        winning_pairs = [[[1, 2], [4, 8], [3, 6]],  # for 0
                         [[0, 2], [4, 7]],  # for 1
                         [[0, 1], [4, 6], [5, 8]],  # for 2
                         [[0, 6], [4, 5]],  # for 3
                         [[0, 8], [1, 7], [2, 6], [3, 5]],  # for 4
                         [[2, 8], [3, 4]],  # for 5
                         [[0, 3], [4, 2], [7, 8]],  # for 6
                         [[6, 8], [1, 4]],  # for 7
                         [[6, 7], [0, 4], [2, 5]],  # for 6
                         ]
        if not isinstance(move, int) or 0 < move > 8 or \
                self.board[move] != 0:
            raise ValueError
        self.playerJustMoved = 3 - self.playerJustMoved
        self.board[move] = self.playerJustMoved
        for index1, index2 in winning_pairs[move]:
            if self.playerJustMoved == self.board[index1] == self.board[index2]:
                self.ended = True

    def GetMoves(self):
        return [] if self.ended else [i for i in range(9) if self.board[i] == 0]

    def GetResult(self, playerjm):
        for (x, y, z) in [(0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7),
                          (2, 5, 8), (0, 4, 8), (2, 4, 6)]:
            if self.board[x] == self.board[y] == self.board[z]:
                if self.board[x] == playerjm:
                    return 1.0
                elif self.board[x] != 0:
                    return 0.0
        if self.GetMoves() == []:
            return 0.5  # draw
        raise ValueError

    def __repr__(self):
        s = ""
        for i in range(9):
            s += '.XO'[self.board[i]]
            if i % 3 == 2: s += "\n"
        return s


class Node:
    """ A node in the game tree. Note wins is always from the viewpoint of playerJustMoved.
        Crashes if state not specified.
    """

    def __init__(self, move=None, parent=None, state=None):
        self.move = move  # the move that got us to this node - "None" for the root node
        self.parentNode = parent  # "None" for the root node
        self.childNodes = []
        self.wins = 0
        self.visits = 0
        self.untriedMoves = state.GetMoves()  # future child nodes
        self.playerJustMoved = state.playerJustMoved  # the only part of the state that the Node needs later

    def UCTSelectChild(self):
        """ Use the UCB1 formula to select a child node. Often a constant UCTK is applied so we have
            lambda c: c.wins/c.visits + UCTK * sqrt(2*log(self.visits)/c.visits to vary the amount of
            exploration versus exploitation.
        """
        s = sorted(self.childNodes, key=lambda c: c.wins / c.visits + sqrt(
            2 * log(self.visits) / c.visits))[-1]
        return s

    def AddChild(self, m, s):
        """ Remove m from untriedMoves and add a new child node for this move.
            Return the added child node
        """
        n = Node(move=m, parent=self, state=s)
        self.untriedMoves.remove(m)
        self.childNodes.append(n)
        return n

    def Update(self, result):
        """ Update this node - one additional visit and result additional wins. result must be from the viewpoint of playerJustmoved.
        """
        self.visits += 1
        self.wins += result

    def __repr__(self):
        return "[M:" + str(self.move) + " W/V:" + str(self.wins) + "/" + str(
            self.visits) + " U:" + str(self.untriedMoves) + "]"

    def TreeToString(self, indent):
        s = self.IndentString(indent) + str(self)
        for c in self.childNodes:
            s += c.TreeToString(indent + 1)
        return s

    def IndentString(self, indent):
        s = "\n"
        for i in range(1, indent + 1):
            s += "| "
        return s

    def ChildrenToString(self):
        s = ""
        for c in self.childNodes:
            s += str(c) + "\n"
        return s


def UCT(rootstate, itermax, verbose=False):
    """ Conduct a UCT search for itermax iterations starting from rootstate.
        Return the best move from the rootstate.
        Assumes 2 alternating players (player 1 starts), with game results in the range [0.0, 1.0]."""

    rootnode = Node(state=rootstate)

    for i in range(itermax):
        node = rootnode
        state = rootstate.Clone()

        # Select
        while node.untriedMoves == [] and node.childNodes != []:  # node is fully expanded and non-terminal
            node = node.UCTSelectChild()
            state.DoMove(node.move)

        # Expand
        if node.untriedMoves != []:  # if we can expand (i.e. state/node is non-terminal)
            m = random.choice(node.untriedMoves)
            state.DoMove(m)
            node = node.AddChild(m, state)  # add child and descend tree

        # Rollout - this can often be made orders of magnitude quicker using a state.GetRandomMove() function
        while state.GetMoves() != []:  # while state is non-terminal
            state.DoMove(random.choice(state.GetMoves()))

        # Backpropagate
        while node != None:  # backpropagate from the expanded node and work back to the root node
            node.Update(state.GetResult(
                node.playerJustMoved))  # state is terminal. Update node with result from POV of node.playerJustMoved
            node = node.parentNode

    # Output some information about the tree - can be omitted
    # if (verbose):
    #     print(rootnode.TreeToString(0))
    # else:
    #     print(rootnode.ChildrenToString())

    return sorted(rootnode.childNodes, key=lambda c: c.visits)[
        -1].move  # return the move that was most visited


def HumanPlayer(state):
    moves = state.GetMoves()
    while True:
        try:
            m = int(input("Your move " + str(moves) + " : "))
            if m in moves:
                return m
        except ValueError:
            pass


def RandomPlayer(state):
    return random.choice(state.GetMoves())


def negamax(board, color, depth):  # ##################################################
    moves = board.GetMoves()
    if not moves:
        x = board.GetResult(board.playerJustMoved)
        if x == 0.0:
            print('negamax ERROR:')
            print(board)
            print(board.playerJustMoved)
            print(board.curr, board.ended)
            print(board.GetMoves())
            raise ValueError
        if x == 0.5:
            return 0.0, None
        if color == 1 and board.playerJustMoved == 1:
            return 1.0, None
        else:
            return -1.0, None
    if depth == 0:
        return 0.0, None
    v = float("-inf")
    best_move = []
    for m in moves:
        new_board = board.Clone()
        new_board.DoMove(m)
        x, _ = negamax(new_board, -color, depth - 1)
        x = - x
        if x >= v:
            if x > v:
                best_move = []
            v = x
            best_move.append(m)
    if depth >=8:
        print(depth, v, best_move)
    return v, best_move


def NegamaxPlayer(game):
        best_moves = game.GetMoves()
        if len(best_moves) != 9:
            _, best_moves = negamax(game, 1, 4)
            print(best_moves)
        return random.choice(best_moves)


if __name__ == "__main__":
    def main():
        random.seed(123456789)
        won = 0
        lost = 0
        draw = 0
        for i in range(10):
            # state = OXOState() # uncomment to play OXO
            state = BigGameState()
            move = 0
            while (state.GetMoves() != []):
                if state.playerJustMoved == 1:
                    # m = RandomPlayer(state)
                    m = UCT(rootstate=state, itermax=100, verbose=False)
                else:
                    # m = UCT(rootstate=state, itermax=100, verbose=False)
                    # m = NegamaxPlayer(state)
                    m = HumanPlayer(state)
                    # m = RandomPlayer(state)
                state.DoMove(m)
                move += 1
                print('Game ', i + 1, ', Move ', move, ':\n', state, sep='')
            if state.GetResult(1) == 1.0:
                won += 1
                print("Player 1 wins!")
            elif state.GetResult(1) == 0.0:
                lost += 1
                print("Player 2 wins!")
            else:
                draw += 1
                print("Nobody wins!")
            print('won', won, 'lost', lost, 'draw', draw)

    start_time = time.perf_counter()
    main()
    total_time = time.perf_counter() - start_time
    print('total_time', total_time)
2 голосов
/ 26 апреля 2019

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

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


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

  1. Сделайте выигрышный ход, если доступно.
  2. Блокируйте победу противника, если доступно.
  3. Возьмите центральную клетку.
  4. Взять угловой квадрат (выбрать случайным образом).
  5. Взять боковой квадрат (выбрать случайным образом).

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


Спасибо Александру Лопатину за то, что он запомнил фатальный недостаток в этом объявлении «никогда».У меня довольно красное лицо, так как я преподавал именно в этом случае по крайней мере три курса:

- - - - - - - 

Александр писал:

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

 0   1   2   3   4   5   6    7 
--- --- --- --- X-- X-- X-X  X-X
--- --- -X- -XO -XO -XO -XO  -XO
--- -O- -O- -O- -O- -OO -OO  OOO

X случайно взял неправильный угловой квадрат в ходе 4. Алгоритм должен играть против возможной развилки в этом ходу.

- - - - - - - 

Сократить снова:

Еще хуже, если вы знаете, что ИИ играет по алгоритму, который я дал, вы можете заставить выиграть, вместо того, чтобы зависеть от случайного выбора угла:

 0   1   2   3   4   5 
--- --- --- --O X-O X-O
--- --- -X- -X- -X- -X-
--- O-- O-- O-- O-- O-O  ... and 'X' cannot block both winning moves.

На 4-м ходу ИИ должен увидеть надвигающуюся вилку и сделать шаг в сторону любым боковым движением (проигрыш в любом углу), что приведет к ничьей.

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