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