前言:
有了上一次Class(一)的学习经验,这次再学习一个新的概念“继承”(inheritance),合在一起就可以实现简单的石头、剪刀、布游戏(简称RPS)。
PS:选择石头剪刀布游戏作为练习也是因为,最近这个我们儿时的游戏,又开始被大家关注,源于浙江大学、浙江工商大学和中科院理论物理研究所的研究人员在预印本网站上发表研究报告(PDF),他们通过实验发现了石头剪刀布的一个制胜策略。引起网民广泛讨论,可到知乎、果核进行搜索。
一、RPS游戏 简要说明
首先,我只是想实现一个简单的人机RPS游戏(主要是为了学习继承)。所以,避开了多人RPS游戏中,需要考虑的分组和解组等复杂的算法问题。(例如:16个人玩的话,如何分组?)
其次,考虑必须创建的类。玩家(Player)是必须创建的类,无论是人还是电脑在游戏中都属于或者继承自这个类。再有,要创建游戏(Game)类,其中包含了选手组(@players_in_game)实例,以及判定游戏胜负的方法等。创建一个游戏对象即开始一场新的游戏。
下面一起来看看代码吧!
二、创建玩家类(PlayerInGame)
player_in_game.rb
class PlayerInGame
attr_reader :name, :player_choice
def initialize(name, player_choice)
@name = name
@player_choice = player_choice
end
def choice_to_num
if player_choice == "rock"
return 0
elsif player_choice == "paper"
return 1
elsif player_choice == "scissors"
return 2
else
return "error"
end
end
def to_s
"I (#{@name}) choice #{@player_choice}"
end
end
创建类和属性自不必再赘述(详见Class(一)),主要关注一下choice_to_num方法。首先,游戏中1v1的两个玩家都会有R/P/S 三个选择(@player_choice),考虑:判定胜负的方法中,@player_choice属性存储的是String对象,我们不可能通过正则表达式去验证两个玩家的选择,再根据选择得出胜负结果,这样太繁琐了。
试想一下,如果将两个玩家@player_choice属性存储的String对象转换成数值对象(Numeric),再通过某种公式计算来判定RPS游戏的胜负结果,这样就简便多了。
从而有了代码中的choice_to_num方法。将玩家的选择(String对象)与数值(Numeric对象)关联起来。
三、电脑玩家类(CompInGame)
comp_in_game.rb
require_relative 'player_in_game'
class CompInGame < PlayerInGame
def initialize(name, player_choice)
super(name, player_choice)
end
def num_to_choice
if player_choice == 0
return "rock"
elsif player_choice == 1
return "paper"
elsif player_choice == 2
return "scissors"
else
return "error"
end
end
def to_s
"#{@name} choice #{self.num_to_choice}"
end
end
和电脑玩RPS游戏,那么电脑也算是玩家。但是电脑和人类总还是有所区别的,放在一个类别里似乎不太恰当。比如:或许电脑会有电量属性,电量不够就无法继续游戏,而人类玩家却不需要电量属性,但是二者又都有一些共同的属性,如:@name、@player_choice。所以有了CompInGame类。也由此引出了一个新的概念继承(inheritance)。
继承(inheritance)
一起来看看Ruby编程语言之父——松本行弘对继承的阐释吧。
随 着 软 件 规 模 的 扩 大 , 用 到 的 类 的 个 数 也 随 之 增 加 , 其 中 也 会 有 很 多 性 质 相 似 的 类 。 这 就 违 背 了 我 们 之 前 强 调 多 次 的 DRY 原 则 。 程 序 会 变 得 重 复 而 且 不 容 易 理 解 。 修 改 程 序 的 代 价 也 会 变 高 , 生 产 力 则 会 降 低 。 所 以 , 如 果 有 把 这 些 相 似 的 部 分 汇 总 到 一 起 的 方 法 就 好 了 。 继 承 就 是 这 种 方 法 。 具 体 说 来 , 继 承 就 是 在 保 持 既 有 类 的 性 质 的 基 础 上 而 生 成 新 类 的 方 法 。 原 来 的 类 称 为 父 类 , 新 生 成 的 类 称 为 子 类 。 子 类 继 承 父 类 所 有 的 方 法 , 如 果 需 要 也 可 以 增 加 新 的 方 法 。 子 类 也 可 以 根 据 需 要 重 写 从 父 类 继 承 的 方 法 。
松本行弘. 松本行弘的程序世界 (Kindle Locations 543-548). 人民邮电出版社.
PS:推荐向我一样的初学者也可以看看《松本行弘的程序世界》,虽然是给高手们的醍醐灌顶之作。但个人感觉对于初学者,其中介绍Ruby起源、面向对象等方面,能够在开始学的过程中就奠定好的基础。
言归正传,对继承的概念有了了解后,回到代码上面。关注如何创建子类。上文中我们讨论了,电脑也算是玩家,与人类玩家有共同的属性,但又有别于人类玩家。那么我们就要创建电脑玩家类所为玩家类的子类。如下:
class CompInGame < PlayerInGame
,即:CompInGame类继承自PlayerInGame类,也可以说:CompInGame类是PlayerInGame类的子类,PlayerInGame类是CompInGame类的父类。
其次,需要关注关键字super。在初始化方法中使用super,即:创建与父类相同的属性。
def initialize(name, player_choice)
super(name, player_choice)
end
四、游戏类(Game)
game.rb
require_relative 'player_in_game'
require_relative 'comp_in_game'
class Game
def initialize
@players_in_game = []
end
def read_in_argv(name, player_choice)
@players_in_game << CompInGame.new("Wall_E", [0, 1, 2].sample)
@players_in_game << PlayerInGame.new(name, player_choice)
end
def to_s
@players_in_game.each do |player|
puts "#{player.name}"
end
end
def rps
20.times { print "=" }
puts
puts "#{@players_in_game[0].name} choice #{@players_in_game[0].num_to_choice}."
puts "#{@players_in_game[1].name} choice #{@players_in_game[1].player_choice}."
20.times { print "=" }
puts
diff = (@players_in_game[0].player_choice - @players_in_game[1].choice_to_num) % 5
if diff == 4 || diff == 2
puts "#{@players_in_game[1].name} wins."
elsif diff == 3 || diff == 1
puts "#{@players_in_game[0].name} wins."
else
puts "Player and Computer tie."
end
puts
end
end
首先,考虑在Game类中需要初始化玩家实例变量,这样才能通过方法对每个玩家的@player_choice进行比较。因此,初始化@players_in_game实例变量,用来存储参加游戏的玩家对象,那么数组自然是最合适的数据类型。
第二、def read_in_argv(name, player_choice)
方法。
read_in_argv方法用于获取终端输入的参数后,自动生成一个电脑玩家对象,同时,根据终端输入的参数生成真人玩家对象。并将两个玩家对象存储到@players_in_game实例变量。
注意一点的是电脑玩家对象的@player_choice属性,指向的并不是字符串对象,而是[0, 1, 2].sample
随机生成的一个数字,随后可以使用电脑玩家类特有的方法def num_to_choice
将数字转换成对应的Rock、Paper、Scissors。
PS:留意一下@players_in_game << CompInGame.new("Wall_E", [0, 1, 2].sample)
,80后的同学是否还记得Wall_E(霹雳五号)呢?
在八十年代颇受欢迎的机器人喜剧,描述一个拥有最精密雷射武器的机器人“五号",在一次短路状况下闯入了热心保护动物的史蒂芬妮家中,并从与人交往的过程中学习到人类的智慧的人性。
第三、def rps
Game类中的rps方法是游戏的核心,因为要通过该方法来判定胜负。
首先,打印出每个玩家的姓名(@name)以及选项(@player_choice),需要注意的是,电脑玩家的@player_choice是数字,所以,需要使用Class CompInGame
类的def num_to_choice
方法,将数字转换成对应的Rock、Paper、Scissors选项。
其次、如何判定胜负?通过将每个玩家@player_choice
属性对应的数值做减法,再做求余运算,最后根据求余的结果使用条件语句判定胜负。
五、来场比赛吧(annual_rps_match)
annual_rps_match.rb
require_relative 'game'
match = Game.new
puts
STDERR.puts "Processing ARGV data"
match.read_in_argv(ARGV[0], ARGV[1])
match.rps
六、问题
为了学习继承,我臆测了这个极简的RPS游戏。但是其中存在漏洞,例如:如果我输入了一个不在范围内的选择(如:飞船),就会报错。或许还有更好地实现RPS游戏的方法。这就需要路过的各位同学给一些建议,我们共同进步。
PS:《纽约时报》提供的猜拳机器人。感兴趣的同学可以玩玩哈!
http://www.nytimes.com/interactive/science/rock-paper-scissors.html
它分为两个难度模式:
初学者(Novice)只会根据你的出拳习惯来猜你下一个会出什么
高难度(Veteran)等级则会从收集了超过二十万场剪刀、石头、布的数据库中, 猜你的下一步会出什么(过了五局之后,就可以点右上的「See What the Computer is Thinking」看计算机是怎么猜的)。