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