实例2:井字棋

井字棋是一种在3 * 3格子上进行的连珠游戏,又称井字游戏。井字棋的游戏有两名玩家,其中一个玩家画圈,另一个玩家画叉,轮流在3 * 3格子上画上自己的符号,最先在横向、纵向、或斜线方向连成一条线的人为胜利方。如图1所示为画圈的一方为胜利者。

图1 井字棋

本实例要求编写程序,实现具有人机交互功能的井字棋。

实例目标

  1. 理解面向对象的思想
  2. 能独立设计类
  3. 掌握类的继承和父类方法的重写

实例分析

根据实例描述的井字棋游戏的规则,下面模拟一次游戏的流程如图2所示。

图2 井字棋游戏流程

图2中的描述的游戏流程如下:

  1. 重置棋盘数据,清理之前一轮的对局数据,为本轮对局做好准备。
  2. 显示棋盘上每个格子的编号,让玩家熟悉落子位置。
  3. 根据系统随机产生的结果确定先手玩家(先手使用X)。
  4. 当前落子一方落子。
  5. 显示落子后的棋盘。
  6. 判断落子一方是否胜利?若落子一方取得胜利,修改玩家得分,本轮对局结束,跳转至第(9)步。
  7. 判断是否和棋?若出现和棋,本轮对局结束,跳转至第(9)步。
  8. 交换落子方,跳转至第(4)步,继续本轮游戏。
  9. 显示玩家当前对局比分。

以上流程中,落子是游戏中的核心功能,如何落子则是体现电脑智能的关键步骤,实现智能落子有策略可循的。按照井字棋的游戏规则:当玩家每次落子后,玩家的棋子在棋盘的水平、垂直或者对角线任一方向连成一条直线,则表示玩家获胜。因此,我们可以将电脑的落子位置按照优先级分成以下三种:

(1)必胜落子位置

我方在该位置落子会获胜。一旦出现这种情况,显然应该毫不犹豫在这个位置落子。

(2)必救落子位置

对方在该位置落子会获胜。如果我方暂时没有必胜落子位置,那么应该在必救落子位置落子,以阻止对方获胜。

(3)评估子力价值

评估子力价值,就是如果在该位置落子获胜的几率越高,子力价值就越大;获胜的几率越低,子力价值就越小。

如果当前的棋盘上,既没有必胜落子位置,也没有必救落子位置,那么就应该针对棋盘上的每一个空白位置寻找子力价值最高的位置落子。

要编写一个评估子力价值的程序,需要考虑诸多因素,这里我们选择了一种简单评估子力价值的方式——只考虑某个位置在空棋盘上的价值,而不考虑已有棋子以及落子之后的盘面变化。下面来看一下在空棋盘上不同位置落子的示意图,如图3所示。

图3 棋盘落子示意图

观察图3不难发现,玩家在空棋盘上落子的位置可分为以下3种情况:

  1. 中心点,这个位置共有4个方向可能和其它棋子连接成直线,获胜的几率最高。
  2. 4个角位,这4个位置各自有3个方向可能和其它棋子连接成直线,获胜几率中等。
  3. 4个边位,这4个位置各自有2个方向可能和其它棋子连接成直线,获胜几率最低。

综上所述,如果电脑在落子时,既没有必胜落子位置,也没有必救落子位置时,我们就可以让电脑按照胜率的高低来选择落子位置,也就是说,若棋盘的中心点没有棋子,则选择中心点作为落子位置;若中心点已有棋子,而角位没有棋子,则随机选择一个没有棋子的角位作为落子位置;若中心点和四个角位都有棋子,而边位没有棋子,则随机选择一个没有棋子的边位作为落子位置。

井字棋游戏一共需要设计4个类,不同的类创建的对象承担不同的职责,分别是:

(1)游戏类(Game):负责整个游戏流程的控制,是该游戏的入口。

(2)棋盘类(Board):负责显示棋盘、记录本轮对局数据、以及判断胜利等和对弈相关的处理工作。

(3)玩家类(Player):负责记录玩家姓名、棋子类型和得分、以及实现玩家在棋盘上落子。

(4)电脑玩家类(AIPlayer):是玩家类的子类。在电脑玩家类类中重写玩家类的落子方法,在重写的方法中实现电脑智能选择落子位置的功能。

设计后的类图如图4所示。

图4 类结构图

本实例中涉及到多个类,为保证程序具有清晰的结构,可以将每个类的相关代码分别放置到与其同名的py文件中。另外,由于Player和AIPlayer类具有继承关系,可以将这两个类的代码放置到player.py文件中。

代码实现

本实例的实现过程如下所示。

  1. 创建项目

使用PyCharm创建一个名为“井字棋V1.0”的文件夹,在该文件夹下分别创建3个py文件,分别为board.py、game.py与player.py,此时程序的目录结构如图5所示。

图5 井字棋文件目录

由于棋盘类是井字棋游戏的重点,因此我们先开发Board类。

  1. 设计Board

(1)属性

井字棋的棋盘上共有9个格子落子,落子也是有位置可循的,因此这里使用列表作为棋盘的数据结构,列表中的元素则是棋盘上的棋子,它有以下三种取值:

  1. " " 表示没有落子,是初始值;
  2. "X" 表示玩家在该位置下了一个X的棋子;
  3. "O" 表示玩家在该位置下了一个O的棋子。

其中列表中的元素为" "的位置才允许玩家落子。为了让玩家明确可落子的位置,需要增加可落子列表。根据图4中设计的类图,在board.py文件中定义Board类,并在该类的构造方法中添加属性board_data和movable_list,具体代码如下。

class Board(object):

"""棋盘类"""

def __init__(self):

self.board_data = [" "] * 9          # 棋盘数据

self.movable_list = list(range(9))  # 可移动列表

(2)show_board()方法

show_board()方法实现创建一个九宫格棋盘的功能。游戏过程中显示的棋盘分为两种情况,一种是新一轮游戏开始前显示的有索引的棋盘,让玩家明确棋盘格子与序号的对应关系;另一种是游戏中显示当前落子情况的棋盘,会在玩家每次落子后展示。在Board类中添加show_board()方法,并在该方法中传递一个参数show_index,用于设置是否在棋盘中显示索引(默认为False,表示不显示索引),具体代码如下。

def show_board(self, show_index=False):

"""显示棋盘

:param show_index: True 表示显示索引 / False 表示显示数据

"""

for i in (0, 3, 6):

print("       |       |")

if show_index:

print("   %d   |   %d   |   %d" % (i, i + 1, i + 2))

else:

print("   %s   |   %s   |   %s" % (self.board_data[i],

self.board_data[i + 1],

self.board_data[i + 2]))

print("       |       |")

if i != 6:

print("-" * 23)

(3)move_down ()方法

move_down ()方法实现在指定的位置落子的功能,该方法接收两个参数,分别是表示落子位置的index和表示落子类型(X或者O)的chess,接收的这些参数都是落子前需要考虑的必要要素,具体代码如下。

def move_down(self, index, chess):

"""在指定位置落子

:param index: 列表索引

:param chess: 棋子类型 X 或 O

"""

# 1. 判断 index 是否在可移动列表中

if index not in self.movable_list:

print("%d 位置不允许落子" % index)

return

# 2. 修改棋盘数据

self.board_data[index] = chess

# 3. 修改可移动列表

self.movable_list.remove(index)

以上代码首先判断落子位置是否可以落子,如果可以就将棋子添加到board_data列表的对应位置,并从movable_list列表中删除。

(4)is_draw ()方法

is_draw ()方法实现判断游戏是否平局的功能,该方法会查看可落子索引列表中是否有值,若没有值表示棋盘中的棋子已经落满了,说明游戏平局,具体代码如下。

def is_draw(self):

"""是否平局"""

return not self.movable_list

(5)is_win ()方法

is_draw ()方法实现判断游戏是否胜利的功能,该方法会先定义方向列表,再遍历方向列表判断游戏是否胜利,胜利则返回True,否则返回False,具体代码如下。

def is_win(self, chess, ai_index=-1):

"""是否胜利

:param chess: 玩家的棋子

:param ai_index: 预判索引,-1 直接判断当前棋盘数据

"""

# 1. 定义检查方向列表

check_dirs = [[0, 1, 2], [3, 4, 5], [6, 7, 8],

[0, 3, 6], [1, 4, 7], [2, 5, 8],

[0, 4, 8], [2, 4, 6]]

# 2. 定义局部变量记录棋盘数据副本

data = self.board_data.copy()

# 判断是否预判胜利

if ai_index > 0:

data[ai_index] = chess

# 3. 遍历检查方向列表判断是否胜利

for item in check_dirs:

if (data[item[0]] == chess and

data[item[1]] == chess

and data[item[2]] == chess):

return True

return False

注意,is_win()方法的ai_index参数的默认值为-1,表示无需进行预判,即提示玩家最有利的落子位置;若该参数不为-1时,表示需要进行预判。

(6)reset_board ()方法

reset_board ()方法实现清空棋盘的功能,该方法中会先清空movable_list,再将棋盘上的数据全部置为初始值,最后往movable_list中添加0~8的数字,具体代码如下。

def reset_board(self):

"""重置棋盘"""

# 1. 清空可移动列表数据

self.movable_list.clear()

# 2. 重置数据

for i in range(9):

self.board_data[i] = " "

self.movable_list.append(i)

  1. 设计Player

根据图4中设计的类图,在player.py文件中定义Player类,分别在该类中添加属性和方法,具体内容如下。

(1)属性

在Player类中添加name、score、chess属性,具体代码如下。

import board

import random

class Player(object):

"""玩家类"""

def __init__(self, name):

self.name = name     # 姓名

self.score = 0       # 成绩

self.chess = None   # 棋子

(2)move()方法

move()方法实现玩家在指定位置落子的功能,该方法中会先提示用户棋盘上可落子的位置,之后使棋盘根据用户选择的位置重置棋盘数据后进行显示,具体代码如下。

def move(self, chess_board):

"""在棋盘上落子

:param chess_board:

"""

# 1. 由用户输入要落子索引

index = -1

while index not in chess_board.movable_list:

try:

index = int(input("请 “%s” 输入落子位置 %s:" %

(self.name, chess_board.movable_list)))

except ValueError:

pass

# 2. 在指定位置落子

chess_board.move_down(index, self.chess)

  1. 设计AIPlayer

根据图4中设计的类图,在player.py文件中定义继承自Player类的子类AIPlayer。AIPlayer类中重写了父类的move()方法,在该方法中需要增加分析中的策略,使得计算机玩家变得更加聪明,具体代码如下。

class AIPlayer(Player):

"""智能玩家"""

def move(self, chess_board):

"""在棋盘上落子

:param chess_board:

"""

print("%s 正在思考落子位置..." % self.name)

# 1. 查找我方必胜落子位置

for index in chess_board.movable_list:

if chess_board.is_win(self.chess, index):

print("走在 %d 位置必胜!!!" % index)

chess_board.move_down(index, self.chess)

return

# 2. 查找地方必胜落子位置-我方必救位置

other_chess = "O" if self.chess == "X" else "X"

for index in chess_board.movable_list:

if chess_board.is_win(other_chess, index):

print("敌人走在 %d 位置必输,火速堵上!" % index)

chess_board.move_down(index, self.chess)

return

# 3. 根据子力价值选择落子位置

index = -1

# 没有落子的角位置列表

corners = list(set([0, 2, 6, 8]).intersection(

chess_board.movable_list))

# 没有落子的边位置列表

edges = list(set([1, 3, 5, 7]).intersection(

chess_board.movable_list))

if 4 in chess_board.movable_list:

index = 4

elif corners:

index = random.choice(corners)

elif edges:

index = random.choice(edges)

# 在指定位置落子

chess_board.move_down(index, self.chess)

  1. 设计Game

根据图4中设计的类图,在game.py文件中定义Game类,分别在该类中添加属性和方法,具体内容如下。

(1)属性

在Game类中添加chess_board、human、computer属性,具体代码如下。

import random

import board

import player

class Game(object):

"""游戏类"""

def __init__(self):

self.chess_board = board.Board()          # 棋盘对象

self.human = player.Player("玩家")        # 人类玩家对象

self.computer = player.AIPlayer("电脑")  # 电脑玩家对象

(2)random_player()方法

random_player()方法实现随机生成先手玩家的功能,该方法中会先随机生成0和1两个数,选到数字1的玩家为先手玩家,然后再为两个玩家设置棋子类型,即先手玩家为“X”,对手玩家为“O”,具体代码如下。

def random_player(self):

"""随机先手玩家

:return: 落子先后顺序的玩家元组

"""

# 随机到 1 表示玩家先手

if random.randint(0, 1) == 1:

players = (self.human, self.computer)

else:

players = (self.computer, self.human)

# 设置玩家棋子

players[0].chess = "X"

players[1].chess = "O"

print("根据随机抽取结果 %s 先行" % players[0].name)

return players

(3)play_round ()方法

play_round ()方法实现一轮完整对局的功能,该方法的逻辑可按照实例分析的一次流程完成,具体代码如下。

def play_round(self):

"""一轮完整对局"""

# 1. 显示棋盘落子位置

self.chess_board.show_board(True)

# 2. 随机决定先手

current_player, next_player = self.random_player()

# 3. 两个玩家轮流落子

while True:

# 下子方落子

current_player.move(self.chess_board)

# 显示落子结果

self.chess_board.show_board()

# 是否胜利?

if self.chess_board.is_win(current_player.chess):

print("%s 战胜 %s" % (current_player.name, next_player.name))

current_player.score += 1

break

# 是否平局

if self.chess_board.is_draw():

print("%s 和 %s 战成平局" % (current_player.name,

next_player.name))

break

# 交换落子方

current_player, next_player = next_player, current_player

# 4. 显示比分

print("[%s] 对战 [%s] 比分是 %d : %d" % (self.human.name,

self.computer.name,

self.human.score,

self.computer.score))

从上述代码可以看出,大部分的功能都是通过游戏中各个对象访问属性或调用方法实现的,这正好体现了类的封装性的特点,即每个类分工完成各自的任务。

(4)start ()方法

start ()方法实现循环对局的功能,该方法中会在每轮对局结束之后询问玩家是否再来一局,若玩家选择是,则重置棋盘数据后开始新一轮对局;若玩家选择否,则会退出游戏,具体代码如下。

def start(self):

"""循环开始对局"""

while True:

# 一轮完整对局

self.play_round()

# 询问是否继续

is_continue = input("是否再来一盘(Y/N)?").upper()

# 判断玩家输入

if is_continue != "Y":

break

# 重置棋盘数据

self.chess_board.reset_board()

最后在game.py文件中通过Game类对象调用start()方法启动井字棋游戏,具体代码如下。

if __name__ == '__main__':

Game().start()

代码测试

运行程序,对战一局游戏的结果如下所示:

|       |

0   |   1   |   2

|       |

-----------------------

|       |

3   |   4   |   5

|       |

-----------------------

|       |

6   |   7   |   8

|       |

根据随机抽取结果 电脑 先行

电脑 正在思考落子位置...

|       |

|       |

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

|       |

|       |

请 “玩家” 输入落子位置 [0, 1, 2, 3, 5, 6, 7, 8]:0

|       |

O   |       |

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

|       |

|       |

电脑 正在思考落子位置...

|       |

O   |       |

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

X   |       |

|       |

请 “玩家” 输入落子位置 [1, 2, 3, 5, 7, 8]:2

|       |

O   |       |   O

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

X   |       |

|       |

电脑 正在思考落子位置...

敌人走在 1 位置必输,火速堵上!

|       |

O   |   X   |   O

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

X   |       |

|       |

请 “玩家” 输入落子位置 [3, 5, 7, 8]:7

|       |

O   |   X   |   O

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

X   |   O   |

|       |

电脑 正在思考落子位置...

|       |

O   |   X   |   O

|       |

-----------------------

|       |

|   X   |

|       |

-----------------------

|       |

X   |   O   |   X

|       |

请 “玩家” 输入落子位置 [3, 5]:5

|       |

O   |   X   |   O

|       |

-----------------------

|       |

|   X   |   O

|       |

-----------------------

|       |

X   |   O   |   X

|       |

电脑 正在思考落子位置...

|       |

O   |   X   |   O

|       |

-----------------------

|       |

X   |   X   |   O

|       |

-----------------------

|       |

X   |   O   |   X

|       |

电脑 和 玩家 战成平局

[玩家] 对战 [电脑] 比分是 0 : 0

是否再来一盘(Y/N)?n

井字棋--课后程序(Python程序开发案例教程-黑马程序员编著-第7章-课后作业)相关推荐

  1. 输入圆的半径计算面积和周长-课后程序(JavaScript前端开发案例教程-黑马程序员编著-第2章-课后作业)

    [案例2-5]输入圆的半径计算面积和周长 一.案例描述 考核知识点 toFixed().isNaN.window.document对象 练习目标 掌握toFixed()方法. 掌握数据类型检测. 了解 ...

  2. 计算圆的面积和周长-课后程序(JavaScript前端开发案例教程-黑马程序员编著-第4章-课后作业)

    [案例4-5]计算圆的面积和周长 一.案例描述 考核知识点 Math.PI().Math.pow() 练习目标 掌握Math.pow()用法. 掌握Math.PI()用法. 了解圆的周长公式. 了解圆 ...

  3. 根据身高体重计算某个人的BMI值--课后程序(Python程序开发案例教程-黑马程序员编著-第3章-课后作业)

    实例3:根据身高体重计算某个人的BMI值 BMI又称为身体质量指数,它是国际上常用的衡量人体胖瘦程度以及是否健康的一个标准.我国制定的BMI的分类标准如表1所示. 表1  BMI的分类 BMI 分类 ...

  4. 判断手机号所属运营商--课后程序(Python程序开发案例教程-黑马程序员编著-第11章-课后作业)

    实例1:判断手机号所属运营商 说到手机号大家并不陌生,一个手机号码由11位数字组成,前3位表示网络识别号,第4~7位表示地区编号,第8~11位表示用户编号.因此,我们可以通过手机号前3位的网络识别号辨 ...

  5. 刮刮乐--课后程序(Python程序开发案例教程-黑马程序员编著-第4章-课后作业)

    实例1:刮刮乐 刮刮乐的玩法多种多样,彩民只要刮去刮刮乐上的银色油墨即可查看是否中奖.每张刮刮乐都有多个兑奖区,每个兑奖区对应着不同的获奖信息,包括"一等奖"."二等奖& ...

  6. python井字棋_用Python做一个井字棋小游戏

    井字棋是一个经典的小游戏,在九宫格上玩家轮流画OXO,当每列或每行或是两个对角成一线时便是获胜. 今天就用Python编写一个井字棋小游戏,与电脑对战. 程序执行画面如下图所示: 程序提供了两种人工智 ...

  7. 利用Minmax搜索设计井字棋AI(python)(带UI)

    利用Minmax搜索设计井字棋AI(python)(带UI) 博弈无处不在,对抗搜索也是人工智能领域一个热点话题,而Minmax搜索也是最基础的对抗搜索方法.闲暇之余小编利用Minmax对抗搜索方法做 ...

  8. sawtooth,井字棋演示和交易族开发流程介绍

    1.实例演示 这里以官网的XO交易族为例演示,该交易族是一个井字棋游戏,在开始之前,我们需要搭建起来一个单节点的sawtooth环境,详情可以查看上一篇博客: Sawtooth,使用docker启动单 ...

  9. HTML+CSS+JavaScript网页制作案例教程-黑马程序员-第五章课后习题(课程介绍专栏效果)

    黑马程序员编著的教材  HTML+CSS+JavaScript网页制作案例教程 第五章:"课程介绍"专栏-课后习题参考代码 题目原型: 请结合给出的素材,运用列表标记,超链接标记以 ...

最新文章

  1. Cocos2D-x工程目录介绍
  2. MySQL5.5读写分离之mysql-proxy
  3. leetcode29. 两数相除(位运算)
  4. foxitreadersdk 打开远程文件_一种最不为人知最简单最方便的用电脑操作手机上的文件...
  5. 114_Power Pivot 销售订单之销售额、成本、利润率相关
  6. 怎么用Python获取全网最全的杰尼龟表情包
  7. bootstrap table 怎么实现前两列固定冻结?
  8. Spring框架----Spring常用IOC注解的分类
  9. Spring.NET 中的 ADO.NET 数据访问的示例
  10. springmvc5源码
  11. rwg52_h头文件注释
  12. java day12第十二课 泛型和枚举
  13. JAVA自学知识点评定标准--自尚学堂马士兵
  14. python中property详解
  15. 条件覆盖(Condition coverage)
  16. Linux杀毒软件之ClamAV
  17. 【Unity开发小技巧】Unity组合键的代码编写
  18. 【Javascript】shift、unshift、pop、push的区别
  19. 易语言超文本ctrl c,易语言超文本浏览框和程序交互源码
  20. 华二紫竹2021年高考成绩查询时间,2019年华二紫竹升学数据分析!

热门文章

  1. 安装mongodb以及使用Robomongo
  2. 自定义View之边框文字,闪烁发光文字
  3. Redis启动与关闭
  4. Fegin的基本使用介绍
  5. pywifi 控制wifi
  6. ue import splat map with world machine
  7. 计算机辅助教育的英语,英语计算机辅助教育运用思路
  8. 计算机科学辞海,《辞海》网络版
  9. 磨金石教育丨九组同色系色彩搭配描绘自然风景
  10. 智能家居市场风起云涌