09-四子棋游戏程序
8.3.1 四子棋游戏程序
四子棋游戏在很多方面都类似于井字棋。这两种游戏都在棋盘网格上进行,都需要玩家把棋子排成一排来赢棋。但由于四子棋的棋盘网格比较大,赢棋的情形有很多,因此棋局的评分过程要复杂很多。
代码清单8-12中的代码有一些貌似很熟悉,但数据结构和评分方法与井字棋完全不同。这两段游戏代码都实现为本章开头介绍的基类 Piece 和 Board 的子类,使得 minimax() 可被这两段游戏代码共享。
代码清单8-12 connectfour.py
from __future__ import annotations
from typing import List, Optional, Tuple
from enum import Enum
from board import Piece, Board, Move
class C4Piece(Piece, Enum):
B = "B"
R = "R"
E = " " # stand-in for empty
@property
def opposite(self) -> C4Piece:
if self == C4Piece.B:
return C4Piece.R
elif self == C4Piece.R:
return C4Piece.B
else:
return C4Piece.E
def __str__(self) -> str:
return self.value
C4Piece 类几乎与 TTTPiece 类完全相同。
接下来是一个函数,用于在指定大小的四子棋棋盘网格中生成可能赢棋的所有网格区段(segment)。具体代码如代码清单8-13所示。
代码清单8-13 connectfour.py(续)
def generate_segments(num_columns: int, num_rows: int, segment_length: int) ->
List[List[Tuple[int, int]]]:
segments: List[List[Tuple[int, int]]] = []
# generate the vertical segments
for c in range(num_columns):
for r in range(num_rows - segment_length + 1):
segment: List[Tuple[int, int]] = []
for t in range(segment_length):
segment.append((c, r + t))
segments.append(segment)
# generate the horizontal segments
for c in range(num_columns - segment_length + 1):
for r in range(num_rows):
segment = []
for t in range(segment_length):
segment.append((c + t, r))
segments.append(segment)
# generate the bottom left to top right diagonal segments
for c in range(num_columns - segment_length + 1):
for r in range(num_rows - segment_length + 1):
segment = []
for t in range(segment_length):
segment.append((c + t, r + t))
segments.append(segment)
# generate the top left to bottom right diagonal segments
for c in range(num_columns - segment_length + 1):
for r in range(segment_length - 1, num_rows):
segment = []
for t in range(segment_length):
segment.append((c + t, r - t))
segments.append(segment)
return segments
上述函数将会返回一个列表的列表,表示棋盘网格中的方位(由列/行组成的元组)。每个子列表包含4个网格方位。这4个网格方位组成的列表被称为一个区段。只要棋盘上有任何区段具有相同的颜色,那么这种颜色的玩家就赢了。
无论是为了检查游戏是否结束(有人赢了),还是为了对棋局进行评分,能够对棋盘中所有区段进行快速搜索都将很有意义。
因此在代码清单8-14所示的代码段中,我们将会缓存给定大小棋盘中的所有区段,存放在 C4Board 类中名为 SEGMENTS 的类变量中。
代码清单8-14 connectfour.py(续)
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 类可能会略微降低一些性能。但是将四子棋棋盘视为7列的组合,在概念上很给力,能够让 C4Board 类的其余部分更加容易编写。具体代码如代码清单8-15所示。
代码清单8-15 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("Trying to push piece to full column")
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 类非常相像。这是有道理的,因为从概念上讲,四子棋的列在游戏过程中就是一个能够压入但从不弹出的栈。但与之前的栈不同,四子棋中的列有一个绝对的限制,即数据项不会超过6个。特殊方法 __getitem__() 也挺有意思,它允许 Column 实例用索引做下标引用。这样 Column 的列表就可以被视为二维列表。请注意,如果底层的 _container 在某些行不包含数据项, __getitem__() 仍会返回一个空棋子。
接下来的4个方法与井字棋游戏程序中的对应方法类似。具体代码如代码清单8-16所示。
代码清单8-16 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() 确定是否有区段包含4个同色棋子。具体代码如代码清单8-17所示。
代码清单8-17 connectfour.py(续)
# Returns the count of black and red pieces in a 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 可以不加改动地使用抽象基类 Board 的 is_draw 属性。
最后,为了对整个棋局进行评分,我们将会对其全部区段进行逐一评分,返回评分的累加结果。同时包含红色和黑色棋子的区段将不得分。包含两个同色棋子和两个空棋子的区段将被视为得1分。包含3个同色棋子得分为100。最后,包含4个同色棋子(有人赢棋)的区段得分为1 000 000。如果该区段属于对手,则得分为负数。 _evaluate_segment() 是一个助手方法,用上述公式对某个区段进行评分。所有经过 _evaluate_segment() 评分的区段,其总分由 evaluate() 生成。具体代码如代码清单8-18所示。
代码清单8-18 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 # mixed segments are neutral
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