通过游戏编程学Python(番外篇)— 单词小测验
通过游戏编程学Python(6)— 英汉词典、背单词
通过游戏编程学Python(番外篇)— 乱序成语、猜单词
文章目录
- 通过游戏编程学Python
- 前言
- 第6个游戏:井字棋
- 1. 玩法简介
- 2. 游戏流程
- 3. 如何表示棋盘和棋子
- 4. 搭出框架
- 4. 决定玩家棋子
- 5. 谁先下?
- 6. 下在哪里?
- 7. 判断胜负
- 8. 是否平局
- 9. 交换玩家
- 总结与思考
前言
从本章开始,我们已经结束了大部分基础知识的讲解,所以重心将回到游戏编程中来。而且为了更好地讲解游戏编程的思路,问哥会改变一下文章的组织架构,会更多地从“创作”的角度去思考某个小游戏,而不是简单地把代码呈现给大家、让大家自己去理解。我会尽量解释为什么要这样写,为什么会需要这个变量、这个循环、这个子程序(自定义函数)。虽然可能有些代码借鉴自其他高手——学习就是这样,模仿是必经之路——但是把它理解透了,知道它是怎么来的,那就成了你自己真正学到的知识了。问哥也是希望努力把这个“理解透”的过程还原出来,让你也能够代入、一起体会创作的乐趣。
今天的游戏借鉴自Al Sweigart的《Python游戏编程快速上手》第11章,但是代码经过问哥自己的理解和重写,变量和函数名称以及功能也会发生一些变化。但对其优秀的设计,比如棋盘与棋子的绘制和人机大战AI的部分都予以了保留,大家一同借鉴。
第6个游戏:井字棋 1. 玩法简介
游戏规则很简单:游戏人数为两人,或可选择人机对战,在一个三乘三的棋盘上轮流落子,棋子先连成一条线者获胜,不管是横、竖、对角。
游戏本身也很简单,先手有着巨大的优势。如果遵循某种规则,你将立于不败之地。实际上,如果在人机大战中给电脑正确的策略,玩家将体会不到半点游戏的乐趣。😃
让我们还是先看看如何用程序来实现吧。
游戏截图:
我们在之前的章节里一直使用流程图,这对我们编写出结构清晰的程序相当有帮助,而且会很方便地在后期对其进行修改,改变游戏规则、优化代码等等。因此,问哥也建议你在编写程序之前,也能够实际用纸和笔把流程画出来。它未必会和我们最终写出来的代码一模一样,正相反,很多时候我们的流程会随着代码编写而不断优化和细化。但至少它让我们清楚地看见方向,节省了更多的时间。
让我们一起先把流程图画出来吧。比如问哥理解的井字棋游戏的初版流程图:
画完图发现,不管玩家1还是玩家2先落子,绝大部分的流程都是一样的,都要做差不多的事情:绘制棋盘、判断胜负、判断是否平局。对应到编程上,电脑最拿手的就是这种重复的工作,我们只要在让电脑开始工作之前告诉它现在是谁在下棋(X还是O)不就好了?于是我们可以对流程再次修改,更新后的流程如下:
这样大概的流程就比较简单了,后面我们在具体程序实现的时候,可能也会对某些细节进行补充,比如某些功能可能需要合并在一个子程序里,而有的功能可能要拆分成几个子程序。
这里我们先不考虑人机大战的部分,我们先把程序框架搭起来,这样后期向里面添加功能将会变得非常容易。
让我们这就开始吧。
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(' | |')
这样的话,如果玩家在某个位置已经落子,就可以清楚地打印在屏幕上。效果如下:
现在大家也能明白为什么没有落子的地方要用空格表示:为了占位。使得空棋盘也能够对齐。
有了流程图,我们可以按图索骥,先把程序框架搭出来。因为是循环落子,必不可少地我们会用到while循环。只有当一方获胜、或者平局,才会跳出循环,结束游戏。
当然,老样子,在结束游戏之前,我们还是必不可少地要询问是否再开一局。
综上所述,搭出程序的框架,并用一对小括号表示这里将要实现的功能:
while True:
pieces = [' '] * 10 # 一局新游戏开始
决定玩家是使用X还是O()
turn = 决定谁先下()
绘制空棋盘()
while True: # 轮流落子的循环
玩家决定下在哪里()
绘制新的棋盘()
if 玩家1或2获胜():
print('玩家1或2获胜')
break
elif 是否平局():
print('平局')
break
else:
交换玩家()
if 不玩了():
break
这里我们就可以注意到:
- 绘制空棋盘()与绘制新的棋盘()所实现的功能是一样,所以只需要一个子程序 绘制棋盘();
- 第一个子程序 决定玩家棋子() 需要返回一个字典,形如{‘X’: ‘玩家1’, ‘O’: '玩家2},用来分辨两名玩家,并方便后面调用;
- 需要一个变量turn来表示当前是谁在下棋,其值为X或O,而且需要把这个变量传参给决定下哪里(),判断获胜玩家() 等子程序里,并在最后通过 交换玩家() 改变turn的值;
- 在 绘制棋盘() 之前,需要先给“棋盘”列表pieces赋值,所以在 玩家决定下哪里() 需要返回一个落子位置。
- 可以直接套用之前我们多次用到的“判断玩家要不要继续玩”的语句。
于是,更新框架并为子程序命名如下:
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] == le and pieces[9] == turn),
(pieces[4] == turn and pieces[5] == le and pieces[6] == turn),
(pieces[1] == turn and pieces[2] == le and pieces[3] == turn),
(pieces[7] == turn and pieces[4] == le and pieces[1] == turn),
(pieces[8] == turn and pieces[5] == le and pieces[2] == turn),
(pieces[9] == turn and pieces[6] == le and pieces[3] == turn),
(pieces[7] == turn and pieces[5] == le and pieces[3] == turn),
(pieces[9] == turn and pieces[5] == le 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。
至此,我们这个双人轮流游戏的井字棋就算是做好了。快叫上你的小伙伴一起试试吧!
总结与思考
今天我们实现了在控制台窗口玩可视化游戏(下棋)的方法,学习了使用列表来表示棋子的思路,更重要的是,问哥身临其境地带大家一步步去思考,如何把它从一个概念,一点点地用程序实现。当然这也还不完全是问哥想要介绍这个游戏的原因,因为还没有介绍到人机大战的部分。
如果把另一个玩家换成电脑,程序就会发生很多变化。比如,我们必须要教会电脑一个策略,让它懂得在哪里落子。而这,听起来就有点人工智能的意思了,当然是最最最简单的人工智能。
碍于篇幅所限、问哥精力有限,不得已把人机大战的部分放到下半部分。也请大家耐心等待,毕竟问哥工作繁忙,也不能每次都肝一万字啊! 😃
谢谢大家读到这里,我们下次再见!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)