通过游戏编程学Python

通过游戏编程学Python(7)— 井字棋(上)通过游戏编程学Python(番外篇)— 单词小测验通过游戏编程学Python(6)— 英汉词典、背单词



文章目录

  • 通过游戏编程学Python
  • 前言
  • 第6个游戏:井字棋(下)
  • 1. 玩法简介
  • 2. 游戏流程
  • 3. 修改框架
  • 4. 电脑的策略
  • 第一步
  • 第二步
  • 之后的策略
  • 5. 完整代码
  • 总结与思考



前言

今天继续完成上篇文章没有讲完的井字棋人机对战的部分。其实单机游戏很多时候最困难的地方是怎样把电脑这个对手培养好,也就是教会电脑怎样在游戏规则内把游戏玩好,做一个和我们旗鼓相当的对手。如果把电脑教得太弱智,也许一开始会有些许成就感,时间久了就会索然无味。如果把电脑教得太完美,同样地,作为人类的我们,便很难再享受到游戏的乐趣。尤其像棋牌类游戏,对人类来说,这种游戏的解法近乎无限,但电脑用它那最擅长的超级计算本领,可以轻松地把所有可能性都推演出来。所以原本斗智斗勇的棋牌游戏,在电脑眼里只是一道简单的计算题。即便是像掷骰子这样看似随机的游戏,计算机也占有优势,因为计算机给出的随机往往也是伪随机。

有点扯远了,其实对于人工智能,或者游戏中的人工智能,是一个大的话题,三言两语无法列出其精华。放到最简单来说,就是怎样把游戏规则告诉给电脑,让它陪我们一起玩游戏。下面,我们一起来看看怎么教会电脑陪我们下这个最简单的井字棋游戏。


第6个游戏:井字棋(下)

1. 玩法简介

游戏规则上篇已经介绍过:游戏人数为两人(或人机),在三乘三的棋盘上轮流落子,棋子横、竖、对角在先连成一条线者获胜。

井字棋python 井字棋python总结感悟_游戏


游戏截图:

井字棋python 井字棋python总结感悟_python_02

2. 游戏流程

如果选择了人机对战,游戏流程会变得稍微不太一样,因为要把“电脑下棋”的部分剥离出来,毕竟电脑不会询问我们要下在哪里,只需要下完棋后画出更新之后的棋盘,再判断出胜负就可以了。所以流程图可以像这样:









No

No



Yes

Yes

游戏开始

玩家选择棋子,执X还是O

抽签决定谁先落子

玩家准备下棋

绘制棋盘

玩家1决定下哪里

绘制棋盘

判断玩家或电脑是否获胜

判断是否平局

交换玩家

电脑决定下哪里

游戏结束


可以看到,流程图的右边,也就是轮到电脑下棋的时候,变得十分简单,只有一个动作,那就是由电脑决定把棋子下在哪里。但实际上,这一个动作里,却包含了我们即将教会电脑的策略,以及一系列推演。下面我们一起来分析一下。

3. 修改框架

首先,由于引入了人机对战,我们不可避免的要修改游戏的主体框架。至少,我们要在游戏的开始,询问玩家是选择双人对战,还是人机对战。如果选择了人机对战,我们要做以下改动:

  1. 在一开始选择棋子的时候屏幕输出的信息要变,也就意味着字典的值要变,之前是{‘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]}
  1. 同样地,当玩家抽签决定谁先下之后(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图形化编程,毕竟这才称得上是电脑游戏。只不过问哥的更新频率可能会稍微放缓,还请各位见谅。同时,问哥会穿插一个新的解题、解谜专题系列,交流和分享一些有趣谜题的解法。方式也会和今天的文章一样,一步步展现解开谜题的思考过程,同时争取做到图文并茂,把谜题讲透彻,力图带给大家身临其境的感觉。希望能够继续得到你们的支持!

好了,感谢你们读到这里,下次再见!