通过游戏编程学Python
通过游戏编程学Python(7)— 井字棋(上)通过游戏编程学Python(番外篇)— 单词小测验通过游戏编程学Python(6)— 英汉词典、背单词
文章目录
- 通过游戏编程学Python
- 前言
- 第6个游戏:井字棋(下)
- 1. 玩法简介
- 2. 游戏流程
- 3. 修改框架
- 4. 电脑的策略
- 第一步
- 第二步
- 之后的策略
- 5. 完整代码
- 总结与思考
前言
今天继续完成上篇文章没有讲完的井字棋人机对战的部分。其实单机游戏很多时候最困难的地方是怎样把电脑这个对手培养好,也就是教会电脑怎样在游戏规则内把游戏玩好,做一个和我们旗鼓相当的对手。如果把电脑教得太弱智,也许一开始会有些许成就感,时间久了就会索然无味。如果把电脑教得太完美,同样地,作为人类的我们,便很难再享受到游戏的乐趣。尤其像棋牌类游戏,对人类来说,这种游戏的解法近乎无限,但电脑用它那最擅长的超级计算本领,可以轻松地把所有可能性都推演出来。所以原本斗智斗勇的棋牌游戏,在电脑眼里只是一道简单的计算题。即便是像掷骰子这样看似随机的游戏,计算机也占有优势,因为计算机给出的随机往往也是伪随机。
有点扯远了,其实对于人工智能,或者游戏中的人工智能,是一个大的话题,三言两语无法列出其精华。放到最简单来说,就是怎样把游戏规则告诉给电脑,让它陪我们一起玩游戏。下面,我们一起来看看怎么教会电脑陪我们下这个最简单的井字棋游戏。
第6个游戏:井字棋(下)
1. 玩法简介
游戏规则上篇已经介绍过:游戏人数为两人(或人机),在三乘三的棋盘上轮流落子,棋子横、竖、对角在先连成一条线者获胜。
游戏截图:
2. 游戏流程
如果选择了人机对战,游戏流程会变得稍微不太一样,因为要把“电脑下棋”的部分剥离出来,毕竟电脑不会询问我们要下在哪里,只需要下完棋后画出更新之后的棋盘,再判断出胜负就可以了。所以流程图可以像这样:
No
No
Yes
Yes
游戏开始
玩家选择棋子,执X还是O
抽签决定谁先落子
玩家准备下棋
绘制棋盘
玩家1决定下哪里
绘制棋盘
判断玩家或电脑是否获胜
判断是否平局
交换玩家
电脑决定下哪里
游戏结束
可以看到,流程图的右边,也就是轮到电脑下棋的时候,变得十分简单,只有一个动作,那就是由电脑决定把棋子下在哪里。但实际上,这一个动作里,却包含了我们即将教会电脑的策略,以及一系列推演。下面我们一起来分析一下。
3. 修改框架
首先,由于引入了人机对战,我们不可避免的要修改游戏的主体框架。至少,我们要在游戏的开始,询问玩家是选择双人对战,还是人机对战。如果选择了人机对战,我们要做以下改动:
- 在一开始选择棋子的时候屏幕输出的信息要变,也就意味着字典的值要变,之前是{‘X’: ‘玩家1’, ‘O’: ‘玩家2’},现在要变成{‘X’: ‘玩家’, ‘O’: ‘电脑’}。为了尽可能少的修改代码,我们再引入一个保存玩家名的列表变量,这样当玩家选择人机对战的时候,字典将会自动调整。这里使用了一个小的while循环用来确保获得玩家正确的输入:如果用户输入’1’或者’2’以外的字符串,列表将为空(还记得我们说过空列表对应的布尔值是False吗?),循环继续。
while True:
pieces = [' '] * 10 # 一局新游戏开始
name_list = []
while not name_list:
s = input('请选择双人还是人机对战?(1-双人 | 2-人机):')
if s == '1':
name_list = ['玩家1', '玩家2']
elif s == '2':
name_list = ['玩家', '电脑']
player_dict = choose_piece(name_list)
# 代码略
相应地,子程序 choose_piece() 也作出以下改动,接收这个玩家名的列表,再通过用户选择得以正确打印出当前游戏玩家名:
def choose_piece(name_list):
piece = ''
while not (piece =='X' or piece == 'O'):
print('请玩家选择使用X还是O?')
piece = input().upper()
if piece == 'X':
return {'X':name_list[0], 'O':name_list[1]}
else:
return {'X':name_list[1], 'O':name_list[0]}
- 同样地,当玩家抽签决定谁先下之后(X还是O),在游戏的主体循环部分(内循环),要检查当前下棋的是电脑还是玩家,由此调用不同的子程序 choose_move() 还是 computer_move()
while True:
# 代码略
while True: # 轮流落子的循环
if player_dict[turn] == '电脑': # 轮到电脑落子
i = computer_move()
else:
i = choose_move(pieces, turn, player_dict)
可以看出,这个新增加的 computer_move() 子程序,就是今天的重头戏:电脑的策略。
4. 电脑的策略
第一步
我们考虑一下如果把电脑换成人类,第一步会考虑怎么落子?这里的第一步,指的并不一定是整个棋局的第一步,因为随着游戏的进行,棋盘上会布满棋子,而我们的程序是循环执行。所以有可能只要再下一颗棋,我们(电脑)就赢了。那如果这时候要我们选,必然会把棋子落在定胜负的位置。换成电脑也是一样,所以我们让电脑首先检查一下,现在的棋盘上有没有下一步就可以获胜的位置。所以在 computer_move() 里首先这样写:
def computer_move():
print('电脑正在落子...')
# 第一步
for i in range(1, 10):
copy = pieces[::]
if copy[i] == ' ':
copy[i] = turn
if is_winner(copy, turn):
return i
电脑首先要做的是从第1到第9个位置逐个检查(piece[0]永远用不到),所以这里使用了for…in循环。并且在循环中检查每个位置是否没有棋子。
随后,为了判断下一步能否获胜,我们需要在一个棋盘的副本上进行推演。使用列表的切片方法 copy = pieces[::]可以创建一个和原列表元素完全相同的副本(千万不要使用 copy = pieces 这样的赋值语句,因为这样做是浅拷贝,会和原列表指向同一个内存地址,任何在copy上做出的改动也会影响到pieces,而我们并不想这样)。
再然后我们让电脑把棋子下在这个副本里(copy[i] = turn),然后交给子程序 is_winner() 判断这一步能否获胜。这里也可以看出,我们要写的这个 computer_move() 至少要接收一个参数turn,来指出现在电脑的棋子是X还是O。
如果有哪一步棋可以直接获胜的话,电脑立即把这个棋子的位置返回给主程序,并不再继续下面的策略。反之,如果所有的地方都不能获胜(在游戏开始的时候必然如此),电脑则进行第二步策略。
第二步
既然自己暂时不能获胜,那下一步当然要阻止对手获胜。于是,我们让电脑检查:如果换做对手,把棋子下在哪里将会直接获胜。
这一步的策略和第一步类似,但唯一的区别是要使用对手的棋子进行推演。于是我们这里添加一个局部变量player_turn,用来代表玩家的棋子。
def computer_move():
# 第二步
player_turn = 'O' if turn == 'X' else 'X'
for i in range(1, 10):
copy = pieces[::]
if copy[i] == ' ':
copy[i] = player_turn
if is_winner(copy, player_turn):
return i
大家可以了解这种把if语句写在一行的写法。如果拆解开来,它的功能等同于:
if turn == 'X':
player_turn = 'O'
else:
player_turn = 'X'
可以看到,除了推演的棋子不同,其他步骤都和第一步一样。同样,在游戏一开始,即便是对手先手,也不可能两颗棋子就赢得游戏。所以这一步在游戏初期依然得不到我们需要的落子点,于是我们让电脑继续选择。
之后的策略
如果前两步都找不到合适的落子的话,百分百是因为现在棋盘上只有2颗以内的棋子,换句话说,也就是游戏刚开始1到2个回合。所以该在哪里落子就有一定的策略性。
这里我们忽略那种紧挨着上一颗棋子的落子方法(比如如果电脑上颗棋子落在位置1,那这一步落在2或4或5),因为在井字棋里,这种策略往往不够“聪明”。
较优的走法,是先考虑把棋子落在四个角的位置1、3、7、9,然后是中心5,最后才是2、4、6、8。(实际上,如果先手落在角的位置,将处于不败之地)
所以我们接下来先检查四个角还有没有空位。如果有,则随机返回一个位置。先创建一个空的列表pos,用于存储可以落子的位置。然后依次检查四个角,如果为空,则存入列表。最后再从列表里随机返回一个数字(落子的位置)
pos = []
for i in [1, 3, 7, 9]:
if pieces[i] == ' ': pos.append(i)
if pos: return random.choice(pos)
由于我们在前面已经引用了random模块,这里就可以直接使用了。
同样地,如果四个角没有空位,优先检查中心5。如果中心也被占了,只能检查剩下的空位(2、4、6、8)了。
if pieces[5] == ' ': return 5
pos = []
for i in [2, 4, 6, 8]:
if pieces[i] == ' ': pos.append(i)
return random.choice(pos)
这样,我们这个电脑下棋的子程序就完成了。请记住:我们在这个子程序里,返回值都是1到9的一个数字,代表的是将要落子的位置。
至此,我们第一个带人工智能的小游戏就完成了。试试看你能不能战胜电脑吧。 😄
5. 完整代码
import random
def draw_board(pieces):
print(' | |')
print(' ' + pieces[7] + ' | ' + pieces[8] + ' | ' + pieces[9])
print(' | |')
print('-----------')
print(' | |')
print(' ' + pieces[4] + ' | ' + pieces[5] + ' | ' + pieces[6])
print(' | |')
print('-----------')
print(' | |')
print(' ' + pieces[1] + ' | ' + pieces[2] + ' | ' + pieces[3])
print(' | |')
def choose_piece(name_list):
piece = ''
while not (piece =='X' or piece == 'O'):
print('请玩家选择使用X还是O?')
piece = input().upper()
if piece == 'X':
return {'X':name_list[0], 'O':name_list[1]}
else:
return {'X':name_list[1], 'O':name_list[0]}
def go_first():
if random.randint(0,1) == 0:
return 'X'
else:
return 'O'
def choose_move(pieces, turn, player_dict):
move = ' '
while move not in '1 2 3 4 5 6 7 8 9'.split() or pieces[int(move)] != ' ':
move = input(f'请{player_dict[turn]}选择落子 (1-9):')
return int(move)
def is_winner(pieces, turn):
con = [
(pieces[7] == turn and pieces[8] == turn and pieces[9] == turn),
(pieces[4] == turn and pieces[5] == turn and pieces[6] == turn),
(pieces[1] == turn and pieces[2] == turn and pieces[3] == turn),
(pieces[7] == turn and pieces[4] == turn and pieces[1] == turn),
(pieces[8] == turn and pieces[5] == turn and pieces[2] == turn),
(pieces[9] == turn and pieces[6] == turn and pieces[3] == turn),
(pieces[7] == turn and pieces[5] == turn and pieces[3] == turn),
(pieces[9] == turn and pieces[5] == turn and pieces[1] == turn)
]
return any(con)
def is_draw(pieces):
for i in range(1, 10):
if pieces[i] == ' ':
return False
return True
def computer_move(pieces, turn):
print('电脑正在落子...')
# 第一步
for i in range(1, 10):
copy = pieces[::]
if copy[i] == ' ':
copy[i] = turn
if is_winner(copy, turn):
return i
# 第二步
player_turn = 'O' if turn == 'X' else 'X'
for i in range(1, 10):
copy = pieces[::]
if copy[i] == ' ':
copy[i] = player_turn
if is_winner(copy, player_turn):
return i
# 先下四个角落,其次中间,最后边线
pos = []
for i in [1, 3, 7, 9]:
if pieces[i] == ' ': pos.append(i)
if pos: return random.choice(pos)
if pieces[5] == ' ': return 5
pos = []
for i in [2, 4, 6, 8]:
if pieces[i] == ' ': pos.append(i)
return random.choice(pos)
# 游戏从此处开始
while True:
pieces = [' '] * 10 # 一局新游戏开始
name_list = []
while not name_list:
s = input('请选择双人还是人机对战?(1-双人 | 2-人机):')
if s == '1':
name_list = ['玩家1', '玩家2']
elif s == '2':
name_list = ['玩家', '电脑']
player_dict = choose_piece(name_list)
turn = go_first()
print(f'{player_dict[turn]}先下棋')
draw_board(pieces)
while True: # 轮流落子的循环
if player_dict[turn] == '电脑':
i = computer_move(pieces, turn)
else:
i = choose_move(pieces, turn, player_dict)
pieces[i] = turn
draw_board(pieces)
if is_winner(pieces, turn):
print(f'{player_dict[turn]}获胜')
break
elif is_draw(pieces):
print('平局')
break
else:
turn = 'X' if turn == 'O' else 'O'
if not input('继续玩吗?(y-继续 | n-退出):').lower().startswith('y'):
break
总结与思考
本章我们实现了人机对战,教会了电脑如何下井字棋。其实在有限解的游戏里,做到这些并不难。把计算和推演的部分教给电脑,它将完美又迅速地完成任务。尤其在像井字棋这样简单的游戏,我们可以使用穷举法,对所有能落子的地方进行逐个比较。而在复杂一些的游戏,比如象棋、围棋等等,设计出更加聪明的电脑了,比如阿尔法狗,却并不容易,往往需要很多人的智慧和努力。问哥不是游戏专业,更不是AI专业,对于这个话题也就点到为止了。但是如何在游戏程序中教会电脑做我们的对手,以后我们还会常常遇到。
最近问哥时常泡在问答频道,见识到不少有趣的python小谜题,加上问哥连续肝了几篇长文,有些体力不支,可能后面会陆续更新一些解题的博文。。。我不是说我们的游戏系列结束了啊(这才哪到哪?),我们毕竟才开了个头,Python的基础知识讲得差不多,下一步会开始介绍GUI图形化编程,毕竟这才称得上是电脑游戏。只不过问哥的更新频率可能会稍微放缓,还请各位见谅。同时,问哥会穿插一个新的解题、解谜专题系列,交流和分享一些有趣谜题的解法。方式也会和今天的文章一样,一步步展现解开谜题的思考过程,同时争取做到图文并茂,把谜题讲透彻,力图带给大家身临其境的感觉。希望能够继续得到你们的支持!
好了,感谢你们读到这里,下次再见!