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

04-井字棋的状态管理

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

8.2.1 井字棋的状态管理

先来开发一些数据结构,以便能跟随井字棋游戏的进度记录其状态。

首先,我们需要一种方法来表示井字棋盘上的每个方格。这里将采用名为 TTTPiece 的枚举类,它是 Piece 的子类。井字棋的棋子可以是 XO 或空(在枚举中用 E 表示)。具体代码如代码清单8-2所示。

代码清单8-2 tictactoe.py

from __future__ import annotations
from typing import List
from enum import Enum
from board import Piece, Board, Move
class TTTPiece(Piece, Enum):
    X = "X"
    O = "O"
    E = " " # stand-in for empty
    @property
    def opposite(self) -> TTTPiece:
        if self == TTTPiece.X:
            return TTTPiece.O
        elif self == TTTPiece.O:
            return TTTPiece.X
        else:
            return TTTPiece.E
def __str__(self) -> str:
    return self.value

TTTPiece 类带有 opposite 属性,并返回一个新的 TTTPiece 。当走完一步井字棋之后,就要从一个玩家的回合翻转到另一个玩家的回合,这时上述设置就比较有用了。每步棋只需要用一个整数来表示,该整数对应于棋盘上可放置棋子的方格。回想一下,在board.py中已将 Move 定义为整数了。

井字棋盘由3行3列组成,共有9个位置。为简单起见,这9个位置可以用一维列表来表示。每个方格的数字表示方案(数组中的索引)可以随意设计,这里将遵照图8-1中所示的方案。

46.png

图8-1 与井字棋盘方格 对应的一维列表索引

棋盘状态将主要保存在 TTTBoard 类中。 TTTBoard 将跟踪记录两种不同的状态:位置(由前面提到过的一维列表来表示)和轮到的玩家。具体代码如代码清单8-3所示。

代码清单8-3 tictactoe.py(续)

class TTTBoard(Board):
    def __init__(self, position: List[TTTPiece] = [TTTPiece.E] * 9, turn: TTTPiece =
     TTTPiece.X) -> None:
        self.position: List[TTTPiece] = position
        self._turn: TTTPiece = turn
    @property
    def turn(self) -> Piece:
        return self._turn

默认棋盘是尚未下过的空棋盘。 Board 的构造函数带有默认参数,将棋局初始化为空,并且是 X 先走(井字棋的开局玩家通常为 X )。大家或许想知道,为什么既有 _turn 实例变量,又有 turn 属性。这是一种技巧,用以确保所有 Board 的子类都能记录当前轮到哪个玩家了。在Python中,没有明确的方式能在抽象基类中指定子类必须包含某个实例变量,但属性有这种机制。

TTTBoard 是一种非正式的不可变数据结构,请勿对 TTTBoard 变量做出修改。每次要走一步棋时,都会生成一个包含每一步的位置变动过的新 TTTBoard 。在本搜索算法中,这种做法将很有用处,因为在搜索分支时,我们就不会无意间对仍处于分析可能走法的棋局做出改动。具体代码如代码清单8-4所示。

代码清单8-4 tictactoe.py(续)

def move(self, location: Move) -> Board:
    temp_position: List[TTTPiece] = self.position.copy()
    temp_position[location] = self._turn
    return TTTBoard(temp_position, self._turn.opposite)

在井字棋游戏中,空的方格都是可落子的。代码清单8-5中的 legal_moves 属性将用列表推导式为给定棋局生成可能的走法。

代码清单8-5 tictactoe.py(续)

@property
def legal_moves(self) -> List[Move]:
    return [Move(l) for l in range(len(self.position)) if self.position[l] == TTTPiece.E]

列表推导式的操作对象是位置列表中的 int 索引。为方便起见(也是有意如此), Move 也被定义为 int 类型,这才使得 legal_moves 的定义能够如此简洁。

为了判断玩家是否赢了游戏,需要扫描井字棋盘的行、列和对角线,扫描的方案有很多种。代码清单8-6中的 is_win 属性的实现代码采用了硬编码方式,看起来就是不停地组合运用了 andor== 操作。这算不上是最漂亮的代码,但能直白地完成任务。

代码清单8-6 tictactoe.py(续)

@property
def is_win(self) -> bool:
    # three row, three column, and then two diagonal checks
    return self.position[0] == self.position[1] and self.position[0] == self.position[2] 
      and self.position[0] != TTTPiece.E or \
    self.position[3] == self.position[4] and self.position[3] == self.position[5]
      and self.position[3] != TTTPiece.E or \
    self.position[6] == self.position[7] and self.position[6] == self.position[8]
      and self.position[6] != TTTPiece.E or \
    self.position[0] == self.position[3] and self.position[0] == self.position[6] 
      and self.position[0] != TTTPiece.E or \
    self.position[1] == self.position[4] and self.position[1] == self.position[7]
      and self.position[1] != TTTPiece.E or \
    self.position[2] == self.position[5] and self.position[2] == self.position[8] 
      and self.position[2] != TTTPiece.E or \
    self.position[0] == self.position[4] and self.position[0] == self.position[8] 
      and self.position[0] != TTTPiece.E or \
    self.position[2] == self.position[4] and self.position[2] == self.position[6] 
      and self.position[2] != TTTPiece.E

如果所有行、列或对角线上的方格都不为空,并且包含相同的棋子,就赢了。

如果没赢也没有地方可落子了,那么这就是平局,抽象基类 Board 已包含平局的属性。最后,我们还需要有评估棋局和将棋盘美观打印出来的方法。具体代码如代码清单8-7所示。

代码清单8-7 tictactoe.py(续)

    def evaluate(self, player: Piece) -> float:
       if self.is_win and self.turn == player:
           return -1
       elif self.is_win and self.turn != player:
           return 1
       else:
           return 0
    def __repr__(self) -> str:
        return f"""{self.position[0]}|{self.position[1]}|{self.position[2]}
-----
{self.position[3]}|{self.position[4]}|{self.position[5]}
-----
{self.position[6]}|{self.position[7]}|{self.position[8]}"""

要想根据已走的棋步一直搜索到游戏结束来确定输赢,这是难以实现的,因此对大多数游戏而言,对某个棋局的评分结果应该是一个近似值。但是井字棋的搜索空间很小,足以从任何棋局开始搜索到结束。因此, evaluate() 方法可以简单地返回一个数字,赢则返回一个最大的数,平局则返回小一点的数,输则返回再小一点的数。