当前位置:嗨网首页>书籍在线阅读

09-四子棋游戏程序

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

8.3.1 四子棋游戏程序

四子棋游戏在很多方面都类似于井字棋。这两种游戏都在棋盘网格上进行,都需要玩家把棋子排成一排来赢棋。但由于四子棋的棋盘网格比较大,赢棋的情形有很多,因此棋局的评分过程要复杂很多。

代码清单8-12中的代码有一些貌似很熟悉,但数据结构和评分方法与井字棋完全不同。这两段游戏代码都实现为本章开头介绍的基类 PieceBoard 的子类,使得 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 可以不加改动地使用抽象基类 Boardis_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