通过游戏编程学Python(7)— 井字棋(上)

通过游戏编程学Python

通过游戏编程学Python(番外篇)— 单词小测验
通过游戏编程学Python(6)— 英汉词典、背单词
通过游戏编程学Python(番外篇)— 乱序成语、猜单词



前言

从本章开始,我们已经结束了大部分基础知识的讲解,所以重心将回到游戏编程中来。而且为了更好地讲解游戏编程的思路,问哥会改变一下文章的组织架构,会更多地从“创作”的角度去思考某个小游戏,而不是简单地把代码呈现给大家、让大家自己去理解。我会尽量解释为什么要这样写,为什么会需要这个变量、这个循环、这个子程序(自定义函数)。虽然可能有些代码借鉴自其他高手——学习就是这样,模仿是必经之路——但是把它理解透了,知道它是怎么来的,那就成了你自己真正学到的知识了。问哥也是希望努力把这个“理解透”的过程还原出来,让你也能够代入、一起体会创作的乐趣。

今天的游戏借鉴自Al Sweigart的《Python游戏编程快速上手》第11章,但是代码经过问哥自己的理解和重写,变量和函数名称以及功能也会发生一些变化。但对其优秀的设计,比如棋盘与棋子的绘制和人机大战AI的部分都予以了保留,大家一同借鉴。


第6个游戏:井字棋

1. 玩法简介

游戏规则很简单:游戏人数为两人,或可选择人机对战,在一个三乘三的棋盘上轮流落子,棋子先连成一条线者获胜,不管是横、竖、对角。
在这里插入图片描述
游戏本身也很简单,先手有着巨大的优势。如果遵循某种规则,你将立于不败之地。实际上,如果在人机大战中给电脑正确的策略,玩家将体会不到半点游戏的乐趣。😃

让我们还是先看看如何用程序来实现吧。

游戏截图:
在这里插入图片描述

2. 游戏流程

我们在之前的章节里一直使用流程图,这对我们编写出结构清晰的程序相当有帮助,而且会很方便地在后期对其进行修改,改变游戏规则、优化代码等等。因此,问哥也建议你在编写程序之前,也能够实际用纸和笔把流程画出来。它未必会和我们最终写出来的代码一模一样,正相反,很多时候我们的流程会随着代码编写而不断优化和细化。但至少它让我们清楚地看见方向,节省了更多的时间。

让我们一起先把流程图画出来吧。比如问哥理解的井字棋游戏的初版流程图:

No
No
No
No
Yes
Yes
游戏开始
玩家选择棋子,执X还是O
抽签决定谁先落子
玩家1准备下棋
绘制棋盘
玩家1决定下哪里
绘制棋盘
判断玩家1是否获胜
判断是否平局
交换玩家
玩家2准备下棋
绘制棋盘
玩家2决定下哪里
绘制棋盘
判断玩家2是否获胜
判断是否平局
游戏结束

画完图发现,不管玩家1还是玩家2先落子,绝大部分的流程都是一样的,都要做差不多的事情:绘制棋盘、判断胜负、判断是否平局。对应到编程上,电脑最拿手的就是这种重复的工作,我们只要在让电脑开始工作之前告诉它现在是谁在下棋(X还是O)不就好了?于是我们可以对流程再次修改,更新后的流程如下:

No
No
Yes
游戏开始
玩家选择棋子,执X还是O
抽签决定谁先落子
玩家1或玩家2准备下棋
绘制棋盘
玩家1或玩家2决定下哪里
绘制棋盘
判断玩家1或玩家2是否获胜
判断是否平局
交换玩家
游戏结束

这样大概的流程就比较简单了,后面我们在具体程序实现的时候,可能也会对某些细节进行补充,比如某些功能可能需要合并在一个子程序里,而有的功能可能要拆分成几个子程序。

这里我们先不考虑人机大战的部分,我们先把程序框架搭起来,这样后期向里面添加功能将会变得非常容易。

让我们这就开始吧。

3. 如何表示棋盘和棋子

摆在我们眼前首要的问题是:如何用程序和数字表示棋盘和棋子。

已知游戏的棋盘是三乘三的方格,很容易让人联想到电脑的小键盘,正好也是9个数字对应棋盘的9个位置。
在这里插入图片描述
于是我们可以考虑让玩家通过小键盘来决定落子在哪里。而棋子也很容易,只有两种符号,我们可以直接使用字母X和O。于是,我们可以用一个列表来保存玩家在9个位置的落子。 而通过列表的索引,就可以直观地写入或读出玩家在棋盘某个位置的落子。比如玩家1在1号位子落子X,玩家2在5号位置落子O,我们就可以用列表表示为:

pieces[1] = 'X'
pieces[5] = 'O'

于是在游戏的一开始,我们要初始化这个列表,用空字符串来表示(马上会解释为什么用空格)该位置没有棋子。而使用10个元素的列表,可以使我们更方便地调用索引,因为列表是从0开始计数,不用使用形如 pieces[i+1]这样的引用。(很显然,pieces[0]永远都不到)

pieces = [' '] * 10

而如何绘制棋盘呢?我们还没有学到图形化GUI,但是因为棋盘很简单,我们完全可以在控制台用文本方式来实现:

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('   |   |')

这样的话,如果玩家在某个位置已经落子,就可以清楚地打印在屏幕上。效果如下:
在这里插入图片描述
现在大家也能明白为什么没有落子的地方要用空格表示:为了占位。使得空棋盘也能够对齐。
在这里插入图片描述

4. 搭出框架

有了流程图,我们可以按图索骥,先把程序框架搭出来。因为是循环落子,必不可少地我们会用到while循环。只有当一方获胜、或者平局,才会跳出循环,结束游戏。

当然,老样子,在结束游戏之前,我们还是必不可少地要询问是否再开一局。

综上所述,搭出程序的框架,并用一对小括号表示这里将要实现的功能:

while True:
    pieces = [' '] * 10 # 一局新游戏开始
    决定玩家是使用X还是O()
    turn = 决定谁先下()
    绘制空棋盘()
    
    while True: # 轮流落子的循环
        玩家决定下在哪里()
        绘制新的棋盘()
        if 玩家12获胜():
            print('玩家1或2获胜')
            break
        elif 是否平局():
            print('平局')
            break
        else:
            交换玩家()
            
    if 不玩了():
        break

这里我们就可以注意到:

  1. 绘制空棋盘()与绘制新的棋盘()所实现的功能是一样,所以只需要一个子程序 绘制棋盘()
  2. 第一个子程序 决定玩家棋子() 需要返回一个字典,形如{‘X’: ‘玩家1’, ‘O’: '玩家2},用来分辨两名玩家,并方便后面调用;
  3. 需要一个变量turn来表示当前是谁在下棋,其值为X或O,而且需要把这个变量传参给决定下哪里()判断获胜玩家() 等子程序里,并在最后通过 交换玩家() 改变turn的值;
  4. 绘制棋盘() 之前,需要先给“棋盘”列表pieces赋值,所以在 玩家决定下哪里() 需要返回一个落子位置。
  5. 可以直接套用之前我们多次用到的“判断玩家要不要继续玩”的语句。

于是,更新框架并为子程序命名如下:

while True:
    pieces = [' '] * 10 # 一局新游戏开始
    player_dict = choose_piece()
    turn = go_first()
    print(f'{player_dict[turn]}先下棋')
    draw_board(pieces)
    
    while True: # 轮流落子的循环
        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 = switch_player()
            
    if not input('继续玩吗?(y-继续 | n-退出):').lower().startswith('y'):
        break

框架搭好,我们后面就只需要把完成各功能的子程序写出来就可以了。

4. 决定玩家棋子

子程序 choose_piece() 完成的功能就是为棋子X和O挑一个“主人”,方便后面下棋的时候知道是谁在下,又是谁赢了。实现代码如下:

def choose_piece():
    piece = ''
    while not (piece =='X' or piece == 'O'):
        print('请玩家1选择使用X还是O?')
        piece = input().upper()
        
    if piece == 'X':
        return {'X':'玩家1', 'O':'玩家2'}
    else:
        return {'X':'玩家2', 'O':'玩家1'}

5. 谁先下?

因为只有两名玩家,可以直接产生一个随机数0或1来决定。又要用到random模块,别忘记在程序的开始引入该模块。

import random

def go_first():
    if random.randint(0,1) == 0:
        return 'X'
    else:
        return 'O'

6. 下在哪里?

在写玩家落子的子程序的时候,我们突然发现,已经落子的地方就不能再落子了,所以我们需要把棋子列表参数pieces传进来,进行判断玩家的选择是否有效。(其实不传这个列表参数也一样,可以当做全局变量调用,但是后期人机大战的机器AI部分会引入一个棋子列表的副本,用于推演每一步的结果,所以这里都把列表传进来,避免后面与要进行比较的副本混淆。)

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)

还要注意,玩家不可以输入0,因为pieces[0]不会被用到,自始至终它都是空格。所以这里使用字符串划分来判断玩家输入的是不是正确数字——玩家只能输入字符串里的9个数字字符。

7. 判断胜负

胜利条件很简单,就是检查横、竖、对角是否一条线都是同一个棋子,总共有8种可能,所以任何一种条件成立,即分胜负。

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)

这里介绍一种写法,就是当我们需要判断多条语句的时候,可以使用内置函数any()或all()。首先定义一个列表,里面放上多条需要判断的语句,比如本例中的8种胜利条件。如果使用any()的话,只要有一个条件为True,则该函数返回True。而all()则代表着必须列表中所有条件都为True,才会返回True。所以,本例中使用any()。又因为其返回的是布尔型True或False,所以可以直接作为我们的自定义函数的返回值。

8. 是否平局

判断是否平局就比较简单了:如果棋盘上已经没有可落子的地方,而又没有分出胜负,那必然就是平局了。所以一个for循环遍历pieces列表,检查是否有元素为空格,就可以实现,同样的,0号位置不用检查:

def is_draw(pieces):
    for i in range(1, 10):
        if pieces[i] == ' ':
            return False
    return True

9. 交换玩家

最后当我们仔细检查的时候,发现或许不需要单独定义一个子程序 switch_player(),因为我们只是要把变量turn的值从‘X’变成‘O’,或从‘O’变成’X’。按照Python的简洁写法,我们使用一条语句,就可以实现:

turn = 'X' if turn == 'O' else 'O'

这条语句相信也比较好理解:如果当前回合turn是O的话,就把它变成X,否则(当turn是X),就把它变成O。

至此,我们这个双人轮流游戏的井字棋就算是做好了。快叫上你的小伙伴一起试试吧!


总结与思考

今天我们实现了在控制台窗口玩可视化游戏(下棋)的方法,学习了使用列表来表示棋子的思路,更重要的是,问哥身临其境地带大家一步步去思考,如何把它从一个概念,一点点地用程序实现。当然这也还不完全是问哥想要介绍这个游戏的原因,因为还没有介绍到人机大战的部分。

如果把另一个玩家换成电脑,程序就会发生很多变化。比如,我们必须要教会电脑一个策略,让它懂得在哪里落子。而这,听起来就有点人工智能的意思了,当然是最最最简单的人工智能。

碍于篇幅所限、问哥精力有限,不得已把人机大战的部分放到下半部分。也请大家耐心等待,毕竟问哥工作繁忙,也不能每次都肝一万字啊! 😃

谢谢大家读到这里,我们下次再见!

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值