Шаг 109.
Задачи ComputerScience на Python. Состязательный поиск. Connect Four. Подключите четыре игровых автомата (окончание)

    На этом шаге мы закончим описание созданных классов.

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

class C4Board(Board):
    NUM_ROWS: int = 6
    NUM_COLUMNS: int = 7
    SEGMENT_LENGTH: int = 4
    SEGMENTS: List[List[Tuple[int, int]]] = \
        generate_segments(NUM_COLUMNS, NUM_ROWS, SEGMENT_LENGTH)

    У класса C4Board есть внутренний класс Column. Он не абсолютно необходим - мы могли бы использовать для представления сетки одномерный список, как сделали для игры в крестики-нолики, или же двумерный список. Кроме того, в отличие от любого из этих решений, применение класса Column, вероятно, немного снизит производительность. Но зато мы получим концептуально мощное представление об игровом поле Connect Four как о группе, состоящей из семи столбцов, что также немного облегчает написание остальной части класса C4Board (файл connectfour.py).

    class Column:
        def __init__(self) -> None:
            self._container: List[C4Piece] = []

        @property
        def full(self) -> bool:
            return len(self._container) == C4Board.NUM_ROWS

        def push(self, item: C4Piece) -> None:
            if self.full:
                raise OverflowError("Попытка переместить в полный столбец")
            self._container.append(item)

        def __getitem__(self, index: int) -> C4Piece:
            if index > len(self._container) - 1:
                return C4Piece.E
            return self._container[index]

        def __repr__(self) -> str:
            return repr(self._container)

        def copy(self) -> C4Board.Column:
            temp: C4Board.Column = C4Board.Column()
            temp._container = self._container.copy()
            return temp

    Класс Column очень похож на класс Stack, который мы использовали в предыдущих шагах. Это имеет смысл, поскольку концептуально во время игры столбец Connect Four представляет собой стек, в который можно помещать данные, но никогда не выталкивать их оттуда. Однако, в отличие от созданных ранее стеков, столбец в Connect Four имеет абсолютный предел в шесть элементов. Также интересен специальный метод __getitem__(), который позволяет получить экземпляр Column по индексу. Это позволяет обрабатывать список столбцов как двумерный список. Обратите внимание: даже если у дублирующего _container нет элемента в какой-то конкретной строке, __getitem__() все равно вернет пустой элемент.

    Следующие четыре метода чем-то похожи на их аналоги для игры в крестики- нолики (файл connectfour.py).

    def __init__(self, position: Optional[List[C4Board.Column]] = None,
                 turn: C4Piece = C4Piece.B) -> None:
        if position is None:
            self.position: List[C4Board.Column] = \
                [C4Board.Column() for _ in range(C4Board.NUM_COLUMNS)]
        else :
            self.position = position
        self._turn: C4Piece = turn

    @property
    def turn(self) -> Piece:
        return self._turn

    def move(self, location: Move) -> Board:
        temp_position: List[C4Board.Column] = self.position.copy()
        for c in range(C4Board.NUM_COLUMNS):
            temp_position[c] = self.position[c].copy()
        temp_position[location].push(self._turn)
        return C4Board(temp_position, self._turn.opposite)

    @property
    def legal_moves(self) -> List[Move]:
        return [Move(c) for c in range(C4Board.NUM_COLUMNS)
                if not self.position[c].full]

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

    # Возвращает количество красных и черных фишек в сегменте
    def _count_segment(self, segment: List[Tuple[int, int]]) -> Tuple[ int, int]:
        black_count: int = 0
        red_count: int = 0
        for column, row in segment:
            if self.position[column][row] == C4Piece.B:
                black_count += 1
            elif self.position[column][row] == C4Piece.R:
                red_count += 1
        return black_count, red_count

    @property
    def is_win(self) -> bool:
        for segment in C4Board.SEGMENTS:
            black_count, red_count = self._count_segment(segment)
            if black_count == 4 or red_count == 4:
                return True
        return False

    Как и TTTBoard, C4Board может использовать свойство is_draw абстрактного базового класса Board без изменений.

    Чтобы оценить позицию, мы по очереди оценим все представляющие ее сегменты, суммируем оценки и вернем результат. Сегмент с красными и черными фишками будет считаться бесполезным. Сегмент, имеющий два поля с фишками одного цвета и два пустых поля, получит 1 балл. Сегмент с тремя фишками одного цвета будет оценен в 100 баллов. Наконец, сегмент с четырьмя фишками одного цвета (победа) набирает 1 000 000 баллов. Если сегмент принадлежит противнику, то его очки вычитаются. Функция _evaluate_segment() - вспомогательный метод, который оценивает сегмент с применением предыдущей формулы. Суммарная оценка всех сегментов методом _evaluate_segment() выполняется с помощью evaluate() (файл connectfour.py).

    def _evaluate_segment(self, segment: List[Tuple[int, int]],
                          player: Piece) -> float:
        black_count, red_count = self._count_segment(segment)
        if red_count > 0 and black_count > 0:
            return 0  # смешанные сегменты нейтральны
        count: int = max(red_count, black_count)
        score: float = 0
        if count == 2:
            score = 1
        elif count == 3:
            score = 100
        elif count == 4:
            score = 1000000
        color: C4Piece = C4Piece.B
        if red_count > black_count:
            color = C4Piece.R
        if color != player:
            return -score
        return score

    def evaluate(self, player: Piece) -> float:
        total: float = 0
        for segment in C4Board.SEGMENTS:
            total += self._evaluate_segment(segment, player)
        return total

    def __repr__(self) -> str:
        display: str = ""
        for r in reversed(range(C4Board.NUM_ROWS)):
            display += "|"
            for c in range(C4Board.NUM_COLUMNS):
                display += f"{self.position[c][r]}" + "|"
            display += "\n"
        return display
Архив с файлом можно взять здесь.

    На следующем шаге мы рассмотрим использование ИИ для Connect Four.




Предыдущий шаг Содержание Следующий шаг