02-棋盘游戏的基础组件
8.1 棋盘游戏的基础组件
与本书中大多数更复杂的问题一样,解决方案应该尽可能保持通用。对于对抗搜索(adversarial search)而言,这意味着搜索算法不能仅适用于某个游戏。下面就从定义一些简单的基类开始,这些基类定义了搜索算法需要用到的所有状态。稍后,我们可以为特定的游戏(井字棋和四子棋)生成该基类的子类,并把子类提供给搜索算法从而开始“玩”游戏。代码清单8-1给出了这些基类。
代码清单8-1 board.py
from __future__ import annotations
from typing import NewType, List
from abc import ABC, abstractmethod
Move = NewType('Move', int)
class Piece:
@property
def opposite(self) -> Piece:
raise NotImplementedError("Should be implemented by subclasses.")
class Board(ABC):
@property
@abstractmethod
def turn(self) -> Piece:
...
@abstractmethod
def move(self, location: Move) -> Board:
...
@property
@abstractmethod
def legal_moves(self) -> List[Move]:
...
@property
@abstractmethod
def is_win(self) -> bool:
...
@property
def is_draw(self) -> bool:
return (not self.is_win) and (len(self.legal_moves) == 0)
@abstractmethod
def evaluate(self, player: Piece) -> float:
...
Move 类型代表游戏中的一步棋(move),本质上它只是一个整数。在井字棋和四子棋之类的游戏中,可以用整数来表示一步棋,指示棋子应该放置在哪个方格或列。 Piece 是一个基类,用于表示游戏棋盘上的棋子。 Piece 还兼有回合指示器的作用,因此它需要带有 opposite 属性。我们必须知道给定回合之后该轮到谁走棋了。
提示 因为在井字棋和四子棋游戏中每人只有一种棋子,所以 Piece 类在本章中可以兼有回合指示器的作用。而对于像国际象棋这种更复杂的游戏,棋子的类型有很多种,回合可以用一个整数或布尔值来指示。或者,更复杂的 Piece 类型也可以用“颜色”属性来指示回合。
棋盘的状态实际是由抽象基类 Board 维护的。针对本章搜索算法将要计算的游戏,我们需要能够回答以下4个问题:
- 该轮到谁走啦?
- 在当前位置上有哪些符合规则的走法?
- 游戏有人赢了吗?
- 游戏平局了吗?
对很多游戏而言,最后的是否平局问题实际上是前两个问题的组合。如果游戏没有人赢,也没有符合规则的棋步可走,那就是出现平局了。因此抽象基类 Game 就带了 is_draw 属性的具体实现。此外,我们还需要能实现以下操作:
- 从当前位置走到新的位置;
- 评估当前位置,看看哪位玩家占据了优势。
Board 的每个方法和属性分别代表了上述某个问题或操作。在游戏术语中, Board 类也可以被称作“棋局”(Position),但这里我们将用该命名来表示每个子类中更具体的内容。