第二章:Python环境准备和预备知识
一、Python编程环境安装
公欲善其事,必先利其器。编程也需要配备相应的工具,让我们编写代码更方便更高效。本书推荐的编程工具是微软开发的免费工具Visual Studio Code,简称为VSCode。它是一个轻量而强大的代码编辑器,它能让我们高效的编辑、调试和运行各类编程语言,其中当然也包括Python语言。其官网地址为 https://code.visualstudio.com/。大家可以从官网下载最新版本的VSCode,并学习相应的文档。
使用VSCode来编写Python,还需要在本地电脑上安装Python核心。为了便于使用Python丰富的模块和管理虚拟环境,本书推荐使用Anaconda来作为管理工具。Anaconda中文名叫做巨蟒,是一个用于科学计算的Python发行版,提供了包管理与环境管理的功能,可以很方便地解决多版本Python并存、切换以及各种第三方包安装问题。它还能使用不同的虚拟环境隔离不同要求的项目,非常的省时省心。其官网为 https://www.anaconda.com/ 。如果你的电脑是64位则尽量选64位版本。Python的版本推荐选择最新版本。
上述两项工具安装完成后,你还需要在VSCode的市场中安装几种VSCode Python扩展插件,在市场中搜索Python即可通过网络直接安装。插件的截图如下。

前面这三项工具安装完成后,可以尝试编写运行一个"Hello World"代码程序。我们在VSCode中新建一个文件夹,将其作为工作目录。然后在目录中创建一个hello.py文件,即Python的源代码文件。在文件中编写如下两行简单的代码,然后就可以直接点击小三角运行。如果能成功在终端中显示信息,说明我们的编程环境已经通过验证了。

二、编写第一个小游戏
本书以游戏为编程主题,那就让我们先尝试编写一个井字棋小游戏,热身起来吧。一方面是复习下我们以前学过的Python知识,另一方面也让我们多熟悉下VSCode。
井字棋游戏很简单,它有一个3*3的格子棋盘,任何一个玩家完成三个棋子在一条线上就胜利了。本游戏程序包含的核心数据就是棋盘信息,另外还需要若干函数来承载游戏功能,例如显示棋盘信息的函数,接收玩家输入的函数,以及判断棋局输赢的函数等。所有代码都在一个代码文件中。
首先定义了几个变量,包括棋盘大小boardSize和棋盘信息board,我们会使用两种符号来表示两位玩家的落子,其中一位是字母符号"X"而另一位是字母符号"O",当棋盘上还没有玩家落子时,所有位置上都是以点号符号“."显示。然后先手玩家是用"X"符号,将其保存在变量currentPlayer中。随后打印一些游戏提示信息。
boardSize = 3
board = ['.'] * boardSize * boardSize
currentPlayer = 'X'
print ("井字棋游戏开始 ")
print ("规则:三子连成线即胜利")
print ("X 先手, O 后手")
随后我们定义print_board函数,它负责在终端上显示棋盘信息。
def print_board(board):
print("\n")
print("%s|%s|%s" % (board[0], board[1], board[2]))
print("-+-+-")
print("%s|%s|%s" % (board[3], board[4], board[5]))
print("-+-+-")
print("%s|%s|%s" % (board[6], board[7], board[8]))
我们还需要一个函数来负责判断胜负。函数hasWon看起来很复杂,其内在逻辑很简单,就是列举出各种可能获胜的棋局状态,只要满足这种状态就返回True。如果觉得某个地方不容易理解,可以通过VSCode设置断点,按下键盘上的F5即可进入调试模式,在调试模式下一步步的运行代码,来观察每个变量的取值并理解其运行逻辑。
def hasWon(currentBoard, player):
winningSet = [player for _ in range(boardSize)]
row1 = currentBoard[:3]
row2 = currentBoard[3:6]
row3 = currentBoard[6:]
if winningSet in [row1,row2,row3]:
return True
col1 = [currentBoard[0],currentBoard[3],currentBoard[6]]
col2 = [currentBoard[1],currentBoard[4],currentBoard[7]]
col3 = [currentBoard[2],currentBoard[5],currentBoard[8]]
if winningSet in [col1,col2,col3]:
return True
diag1 =[currentBoard[0],currentBoard[4],currentBoard[8] ]
diag2 =[currentBoard[6],currentBoard[4],currentBoard[2] ]
if winningSet in [diag1,diag2]:
return True
return False
因为是两个玩家交替落子,需要定义getNextPlayer函数以交换当前玩家。
getPlayerMove函数用来负责获取玩家的输入。玩家输入的格式是坐标的形式,每次需要输入两个数字,数字之间以英文逗号间隔,数字取值在0到2之间。意思就是落子位置在棋盘上的第x行,第y列。需要注意的是,获取玩家输入后,会将这些输入信息通过字符串操作拆分成两部分,再转换成整数类型保存坐标。再将二维坐标信息转换成一维的索引。判断当前棋盘的落子位置上是空位时,就用当前玩家的符号进行赋值。
def getPlayerMove(board, currentPlayer):
isMoveValid = False
while isMoveValid == False:
print('')
userMove = input(f'玩家 {currentPlayer} 输入棋盘坐标(坐标取值0,1,2): X,Y? ')
userX, userY = [int(char) for char in userMove.split(',')]
userIndex = userX * boardSize + userY
if board[userIndex] == '.':
isMoveValid = True
board[userIndex] = currentPlayer
return board
当棋盘上摆满的棋子时,我们需要结束游戏,因此定义hasMovesLeft函数,来判断还有没有空白位置供玩家落子。
最后将这些函数组装在一起。
if __name__ == '__main__':
print_board(board)
while hasMovesLeft(board):
board = getPlayerMove(board, currentPlayer)
print_board(board)
if hasWon(board, currentPlayer):
print('Player ' + currentPlayer + ' has won!')
break
currentPlayer = getNextPlayer(currentPlayer)
首先显示棋盘,只要棋盘上还有地方下棋,我们就保持游戏主循环,循环中先获取玩家输入,显示新的棋盘,如果出现输赢结果,我们就把信息显示出来。如果还没有输赢,就轮换玩家。游戏画面如下图所示。
这个游戏是不是很简单。你可以先试着玩一下。如果你不能很好的理解这些代码,可以进入调试模式慢慢理解每行代码的任务。如果是对于某些语法不熟悉,可以去复习巩固一下对应的知识。
三、面向对象编程
上面实现井字棋的代码体现了一种过程化的编程设计思路。例如,从接收用户输入到判断胜负,再到显示新的棋盘。将整体游戏逻辑根据这些步骤拆开,然后分别用函数实现每个过程的逻辑。当程序非常小的时候,基于过程的设计思路没什么问题。但当程序功能越来越复杂时,我们需要一种更灵活的设计思路,也就是面向对象编程,英文术语称为Object Oriented Progamming, 通常简写为OOP。
面向对象编程的观点是不应该将程序分割为若干步骤任务,而是将其分解为自然对象的模型。OOP涉及到几个关键概念,包括类、对象、组件、属性和行为。我们分别来解释一下。类可以看作是一张蓝图或图纸,代表了抽象概念,而对象是具体的事物。类和对象的关系是抽象和具体的关系,就好象马这个词所代表的抽象概念,和某一匹在草原上正在奔驰的骏马,它们二者之间的关系就是抽象和具体的关系。古代战国所说的白马非马的故事,就是指抽象和具体的区别。
一个复杂的类可能是由多个组件构成的。例如汽车就是由引擎、车身、底盘等组件构成的。这些组件本身也是一个类,例如引擎类或底盘类。类是有属性的。属性是类的数据特征,例如马是有颜色的,颜色是马这个类的属性,具体到某一匹马,有的马是白色,有的马是黑色。不同的具体的马的对象,可以有不同的属性值。类也是有行为的。行为定义了类可以做什么事,马可以奔跑,这是马的一种功能,或者说是一种函数。
面向对象编程的思路体现了一种模块化建造、分而治之的的思路,就像是造汽车,工厂根据不同部件的关联程度分成几个模块,分发到几个不同的车间去建造。例如发动机车间专门造引擎,底盘车间专门造底盘,然后总装车间根据接口将它们组装起来,各模块只需专注自己部分的建造,互不干扰。而且在出新款车型时,可能只需要更改底盘,而旧的引擎仍可以重复使用。面向对象编程的的优点归纳如下:
- 分别用类来封装各自的数据和函数,代码相对独立,更容易修改和管理。
- 让编程思路更清晰,编写更高效,更容易理解,不容易出错。
- 可以直接使用现成的类,代码能更好的重用。
还是以上面的井字棋游戏为例。在面向对象的设计思路下,游戏本身是一个类,某个特定的游戏是一个对象,它是这个类的特定实例。游戏这个类中就包括了一个重要的组件,也就是棋盘类。棋盘类包含两个属性,也就是棋盘大小和棋盘空间本身,棋盘类也包括若干函数,例如显示棋盘、判断胜负等等。游戏类中包括了前面说的棋盘类作为组件,还有一个属性就是当前需要落子的玩家。另外也包括了若干函数,例如处理玩家输入等。
我们可以根据游戏逻辑,将游戏程序用类图的形式进行重新设计。如下是设计好的Game类和Board类的类图。类图中显示了每个类中包含的属性和函数。可以让我们更清楚的检查类的设计,以及类之间的关系。
classDiagram
Game *-- Board : Composition
class Game {
String currentPlayer
Board board
getNextPlayer()
getPlayerMove()
play()
}
class Board {
Int size
list contain
show()
hasMovesLeft()
isMoveValid(x,y)
setMove(x,y,player)
hasWon(player)
}
下面我们根据面向对象的设计思路来修改原有的代码。首先是定义棋盘类,也就是Board类。在初始化函数中定义了两个属性,分别是棋盘大小size,以及棋子信息pieces。
棋盘拥有显示函数show。
def show(self):
print("\n")
print("%s|%s|%s" % (self.pieces[0], self.pieces[1], self.pieces[2]))
print("-+-+-")
print("%s|%s|%s" % (self.pieces[3], self.pieces[4], self.pieces[5]))
print("-+-+-")
print("%s|%s|%s" % (self.pieces[6], self.pieces[7], self.pieces[8]))
也拥有用于判断有没有落子空间的hasMovesLeft函数。
棋盘类还需要有一个isMoveValid函数用于判断当前落子位置是否合法,以及落子后修改棋子信息setMove函数。因为用户输入的是一个二维的坐标,所以需要建立一个辅助函数locToMove,负责从二维棋盘坐标转换到一维的列表索引。
def locToMove(self, loc):
return int(loc[1] + loc[0] * self.size)
def isMoveValid(self,loc):
move = self.locToMove(loc)
if self.pieces[move] == '.':
return True
else:
return False
def setMove(self,loc,player):
move = self.locToMove(loc)
self.pieces[move] = player
当然也包括判断棋局胜负的hasWon函数。这里的实现逻辑和旧代码一样。
def hasWon(self,player):
winningSet = [player for _ in range(self.size)]
row1 = self.pieces[:3]
row2 = self.pieces[3:6]
row3 = self.pieces[6:]
if winningSet in [row1,row2,row3]:
return True
col1 = [self.pieces[0],self.pieces[3],self.pieces[6]]
col2 = [self.pieces[1],self.pieces[4],self.pieces[7]]
col3 = [self.pieces[2],self.pieces[5],self.pieces[8]]
if winningSet in [col1,col2,col3]:
return True
diag1 =[self.pieces[0],self.pieces[4],self.pieces[8] ]
diag2 =[self.pieces[6],self.pieces[4],self.pieces[2] ]
if winningSet in [diag1,diag2]:
return True
return False
然后我们在棋盘类基础上,再构造游戏类,即Game类。Game类的初始化函数中包括了属性当前玩家,以及上面定义好的Board类,将其进行实例化,把棋盘对象作为游戏的一个组件。
class Game:
def __init__(self,boardSize,startPlayer):
self.currentPlayer = startPlayer
self.board = Board(boardSize)
print ("井字棋游戏开始 ")
print ("规则:三子连成线即胜利")
print ("X 先手, O 后手")
Game类中包括了轮换玩家的getNextPlayer函数。
@staticmethod
def getNextPlayer(currentPlayer):
if currentPlayer == 'X':
return 'O'
else :
return 'X'
def getPlayerMove(self):
while(True):
userMove = input(f'\n 玩家 {self.currentPlayer} 输入棋盘坐标(坐标取值0,1,2): X,Y? ')
userMoveLoc = [int(char) for char in userMove.split(',')]
if self.board.isMoveValid(userMoveLoc):
self.board.setMove(userMoveLoc,self.currentPlayer)
break
def play(self):
self.board.show()
while self.board.hasMovesLeft():
self.getPlayerMove()
self.board.show()
if self.board.hasWon(self.currentPlayer):
print('\n 玩家 ' + self.currentPlayer + ' 胜利!')
break
self.currentPlayer = self.getNextPlayer(self.currentPlayer)
四、使用Python模块
编程领域常说的一句话是不要重复造轮子,因此重用已有的代码是编程设计的重要方面。Python中有大量已经非常完善的类和函数。这些可以重用的代码称为模块。在Python中进行游戏编程开发依赖的模块是PyGame,这个模块将在第三章进行介绍。进行AI编程开发的模块是PyTorch,这个模块将在第八章进行介绍。我们还会依赖其它一些模块,这些模块我们先做一些简单介绍。大家还可以通过官网去查阅更详细的模块资料。如果本地环境还没有安装这个模块,需要通过pip联网进行安装,具体操作方法可以很容易的搜索得到。
random模块
和名字说的那样,random模块是用于生成伪随机数。下例中,使用random模块的randint函数,可以生成从1到10之间任意一个整数数字。随机数字的引入可以增强游戏的随机性,让游戏更为有趣。
Numpy模块
Python中内置了很多数据类型,例如列表、字典、集合等,它们都可以保存一系列的数据信息。不过对于大规模的数据计算,它们并不都适合。在人工智能领域的计算中,这种大规模数据一般以向量形式保存。Numpy模块是专门用于向量计算的模块。让我们看看下面的例子。
示例中首先建立一个列表,再建立一个数组,打印观察二者的类型,可以看到类型是不一样的。x_list是列表list类型,x_array是Numpy的数组类型。
list
numpy.ndarray
Numpy数组类型的重要特点在于支持向量化计算,如果我们要计算这些数值的平方再求和,如果是用列表的数据类型,需要写一个循环,或是使用列表解析。
30
30
如果是Numpy数组类型,则更加简单,它可以直接对每个元素做平方操作。这个例子可以看到Numpy数组的优点,在于向量化运算非常方便快速。
30
matplotlib模块
matplotlib是Python环境中非常重要的绘图模块。因为AI和大规模数据计算有直接关系,我们会主要使用绘图功能来观察数据中的规律,在游戏领域中观察各项AI指标的走向。绘图模块的功能非常丰富,我们先用一个例子来了解一下。
先通过Numpy模块来构造两个向量,x和y,x形成了横坐标的位置,y形成了sin函数的大小。
使用linspace从0开始到\(2\pi\)生成100个点,然后用向量化计算直接算出100个点对应的sin值。再使用plt.plot,就绘制了基础的线图,表现了两个变量之间的函数关系。这种线图就是将数据的大小反映到坐标位置上,我们很容易观察出sin函数的这种规律性波动变化。

copy模块
Python有一个很重要的特点,就是当我们使用等号进行赋值时,并没有拷贝动作,而仅仅是进行了一次绑定操作。我们看看下例。
可以看到x的第一个元素修改后,y也改变了,因为它们都绑定的是同一个list对象。y=x这句代码,只是让y这个变量和已有的列表对象进行了绑定,或者说y只是x的引用。如果一定要进行拷贝操作,那就需要使用copy模块。使用copy模块中的copy函数,y就完全复制了一份,不会被x的修改动作所影响。
当x是复合对象时,例如嵌套列表,情况又复杂了,此时copy函数并不完整的复制,因为它只能进行所谓的浅拷贝。
对于这种情况,我们需要用另一个函数deepcopy,来进行所谓的深拷贝。
collections模块
我们还会使用collectioins模块中的一些数据结构,例如namedtuple类型,它是带有名字的元组,下例中我们定义了一个几何意义上的点坐标,如果是用传统的元组,需要使用数字编号来取值,使用名字让代码更容易理解。
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(11, 22)
print(p[0] + p[1])
print(p.x + p.y)
另一个会用到的是deque类型,它称为队列,它和列表一样,可以被遍历,可以增加元素。它的一个重要特点在于,当指定了最大容量后,新增加的元素补充进右侧尾部,最左侧头部的元素则会被删除。这样实现了先进先出的设计需求。
from collections import deque
d = deque('ghi',maxlen=4)
d.append('j')
print(d)
d.append('k')
print(d)
本章小结
本书用Python语言编写代码,因此在本章介绍了如何准备一个Python编程环境,也就是VSCode加上Anaconda。然后带领大家编写了第一个入门小游戏,这是一个以终端为输出界面的井字棋小游戏。大家可以用这个示例来熟悉VSCode的编辑调试功能。最初的代码并不是很完善,我们基于面向对象的设计思路重构了井字棋的代码。大家可以观察重构前后的代码差别,来体会理解OOP。通过这个实例,我们还学习了Python中关于类的编写方法。最后我们介绍了几个会用到的Python模块。在第三章我们会开启PyGame的学习,这个模块会帮助我们开发真正有趣的游戏,我们会大量的使用类和对象。你将会学到很多新奇的玩意。