如果打算开发一款围棋游戏,该怎么着手去做呢?本文是针对《深度学习与围棋》第三章的python代码进行了思路梳理,是为了加深对作者代码的理解。
把握围棋游戏的主体流程
首先你得懂围棋,最好自己会玩,但无需你是围棋高手,哪怕是刚入门的新手也足够了。整体来说,围棋游戏由两位棋手在19x19(也可以是9x9、13x13)个交叉点网格组成的棋盘上轮流落子,最终围地最多的棋手胜出。我们可以尝试通过伪代码的形式来表示围棋游戏的过程。
1 | # 创建一个棋盘规格为board_size*board_size的新游戏对象 |
这部分伪代码虽然十分粗略,它们只是把围棋游戏的过程翻译成了代码的形式而已,根本就没有具体的实现,但是通过这段伪代码我们可以识别出关键的对象和方法。其中new_game用来初始化游戏对象,游戏对象显然需要存储棋盘信息、游戏的状态及历史状态,下回合执子方信息。另外game对象通过is_over方法来判断游戏是否结束,apply_move方法来执行落子动作,因为一旦落子肯定会影响游戏的状态,需要进行处理并返回更新了游戏状态的游戏对象。move表示落子动作,围棋一个回合包含落子、跳过、认输三个选项。print_board、print_move为辅助方法,用来实时刷新围棋棋盘以及棋手的落子信息。
定义基础的数据类型
计算机科学就是建立在抽象的基础之上的,操作系统是对计算机硬件的抽象,它屏蔽了硬件的复杂性和多样性,让用户可以以统一的方式操作各种各样的计算机硬件,抽象降低了复杂度,如今的计算机发展就是建立在一级级地抽象基础之上的,让我们不必从初级的层次开始我们的工作。接下来我们要做的是也是建立抽象,围棋游戏就是和棋子、棋盘打交道,我们需要为棋子、棋盘、棋盘上的交叉点、一次回合的动作等建立抽象。
- Player为棋子的抽象,它就是一个枚举类型,让我们可以以易读的方式表示棋子,Player.white表示白棋、Player.black表示黑棋、Player.black.other就是表示白棋
1
2
3
4
5
6
7
8
9import enum
class Player(enum.Enum):
black = 1
white = 2
def other(self):
return Player.black if self == Player.white else Player.white - Point是对棋位的抽象,我们知道棋盘的任意一个棋位可以通过行、列来唯一确定,如p=Point(2,3)表示为C2棋位,方便的是可以通过p.row、p.col来访问行列的值,另外还定义了neighbors方法来方便地得到该棋位相邻的四个棋位。
1
2
3
4
5
6
7
8
9
10
11from collections import namedtuple
# Point对象来指定落子的位置,如Point(2,3)表示落子在第2行、第3列
class Point(namedtuple('Point', 'row col')):
def neighbors(self):
return [
Point(self.row - 1, self.col),
Point(self.row + 1, self.col),
Point(self.row, self.col - 1),
Point(self.row, self.col + 1),
] - Move是一个回合动作的抽象,每个回合有落子、跳过、认输三个选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# Move表示落子动作,只包含落子、跳过、认输三种类型的动作
class Move():
def __init__(self, point=None, is_pass=False, is_resign=False):
# ^是异或操作,下面的语句表示三个条件只能有一个为真
assert (point is not None) ^ is_pass ^ is_resign
self.point = point
self.is_play = (self.point is not None)
self.is_pass = is_pass
self.is_resign = is_resign
def play(cls, point):
# 在棋盘上落子
return Move(point=point)
def pass_turn(cls):
# 跳过回合
return Move(is_pass=True)
def resign(cls):
# 认输
return Move(is_resign=True)
棋盘Board类、游戏状态GameState类的实现
接下来就是利用上面的基本类型来实现棋盘类Board、游戏状态类GameState,Board类表示棋盘、负责落子和吃子的逻辑。GameState表示棋局游戏状态,它负责存储棋盘状态、下一回合执子方、上一回合棋盘状态、上一步的动作。可以这么理解,Board对象表达是当前的棋盘快照,而GameState存储了所有回合的棋盘的快照,也就是说GameState表达了整个围棋游戏的过程,它可以实现围棋的复盘。
先来实现Board类,但是开始前我们还需要想清楚该怎么跟踪棋盘上棋子的信息,我们知道每个棋位只有三种可能:空位、黑子、白子,最简单直接的办法当然是每个棋位单独保存独立棋子信息即可,但是直观地感觉这种方式并不是好,因为围棋中最重要的是气,棋子连接起来有两个眼算算作活棋,也就是说棋子之间是有联系的。比较好的方式是跟踪棋链,而不是单独的棋子。棋链是棋盘上相连的一片同色棋子,那么我们就需要建立一个棋链的抽象类GoString,它负责跟踪棋链中的棋子以及棋子的气,还提供增加和减少气数的方法。采用棋链的方式,Board获取每个棋位得到将是这个棋位所在的棋链对象。
1 | # 棋链表示一组相同颜色且相连的棋子, 跟踪维护自身的气数 |
接下来,Board类的实现就顺理成章了,Board类最重要的方法是place_stone,负责落子逻辑的实现,其他都是辅助方法。落子后可能发生双方棋子气数的变化或吃掉对方的子,因此落子需要做合并相邻的棋链、减少对方棋链的气、提走对方气为0的棋链的处理,实现这些使用的都是棋链对象提供的方法。另外Board对象的私有变量_grid存储了整个棋盘的棋链信息,它的key值就是每个棋位的坐标Point(row, col),从这里也可以看出Board对象就是围棋游戏当前回合的一个快照。
1 | # 棋盘类,棋盘尺寸由行数和列数两个决定 |
最后是GameState类,它核心的功能是保存了每一个回合的棋盘快照,下一个回合的执子方以及上一步的动作。另外,GameState还负责实现禁止自吃规则、劫争规则的实现,这些规则之所以不在Board中实现,是因为Board类是单个回合的棋盘快照,而禁止自吃和劫争需要多个棋盘快照的信息才能确定是否满足规则,比如禁止自吃需要当前回合棋盘和落子后新的棋盘对比才能判断,而劫争规则则需要与历史棋盘比对,单个的棋盘对象显然不具备这个能力。
1 | # 存储围棋游戏的状态 |
开始对弈
至此,一款围棋游戏的基本逻辑就建立好了(棋盘的显示和落子动作的显示等辅助方法因为比较简单就没有提及),最后就是要实现对弈程序了。可以轻松实现人机对弈以及机器人自我对弈,但是这里的机器人是完全随机选择合法的落子动作,水平是很弱的,要实现更智能的对弈机器人需要大量的工作,但是围棋的基本逻辑并没有改变。下面是机器人自我对弈的示例代码,其他如人机对弈也是类似的,自行查看代码即可。
随机落子机器人代码
1 | import random |
机器人自我对弈代码
1 | from dlgo.agent import naive |
自我对弈终盘结果
1 | 9 x x x x x x x x x |