テニスの取得ゲーム状況


キミのコードが汚い理由 − @IT をうけた、Haskell 脳その他の方々による Ruby 版などを拝見。


それぞれに興味深かったので、私なりのメッセージング脳だとどのようになるのか、Smalltalk 版と、それをほぼ直訳した Ruby 版を書いてみました。


まずベースとなる Smalltalk 版をメタ情報を除いた加工コードで(ファイルイン不可)。

Object subclass: #TennisSetScorer
   instanceVariableNames: 'games'

TennisSetScorer >> initialize
   games := Dictionary new.
   games at: #player1 put: 0.
   games at: #player2 put: 0

TennisSetScorer >> gameWonBy: player
   games at: player put: (games at: player) + 1

TennisSetScorer >> leader
   ^ games keyAtValue: self leaderGames

TennisSetScorer >> leaderGames
   ^ games values max

TennisSetScorer >> opponentGames
   ^ games values min

TennisSetScorer >> isComplete
   ^ self leaderGames = 7 or: [(self leaderGames = 6) & (self opponentGames < 5)]

TennisSetScorer >> isTied
   ^ self leaderGames = self opponentGames

TennisSetScorer >> stateString
   | info |
   info := {self leader capitalized. self leaderGames. self opponentGames}.
   self isTied ifTrue: [^ 'Set is tied at ', games values anyOne asString].
   self isComplete ifTrue: [^ '{1} wins the set {2} - {3}' format: info].
   ^ '{1} leads {2} - {3}' format: info
| set |
set := TennisSetScorer new.
World findATranscript: nil.
[set isComplete] whileFalse: [
   set gameWonBy: #(player1 player2) atRandom.
   Transcript cr; show: set stateString]

出力例。

Player2 leads 1 - 0
Set is tied at 1
Player2 leads 2 - 1
Set is tied at 2
Player1 leads 3 - 2
Set is tied at 3
Player2 leads 4 - 3
Player2 leads 5 - 3
Player2 leads 5 - 4
Set is tied at 5
Player1 leads 6 - 5
Set is tied at 6
Player1 wins the set 7 - 6


これを、Ruby に直訳すると…

class TennisSetScorer
  def initialize; @games = {:player1 => 0, :player2 => 0} end

  def game_won_by(player); @games[player] += 1 end

  def leader; @games.index(leader_games) end
  def leader_games; @games.values.max end
  def opponent_games; @games.values.min end

  def is_complete; leader_games == 7 or (leader_games == 6 && opponent_games < 5) end
  def is_tied; leader_games == opponent_games end

  def state_str
    leader_str = leader.to_s.capitalize
    if self.is_tied
      "Set is tied at #{leader_games}"
    elsif self.is_complete
      "#{leader_str} wins the set #{leader_games} - #{opponent_games}"
    else
      "#{leader_str} leads #{leader_games} - #{opponent_games}"
    end
  end
end

class Array; def at_rand; self[rand(size)] end end

set = TennisSetScorer.new
until set.is_complete do
  set.game_won_by([:player1, :player2].at_rand)
  puts set.state_str
end


しいて方針めいたものを書くとすれば、代入よりはこまめにメソッド化して #state_str の中味を簡素化、かつ、#is_tied や #is_complete を介してより叙述的にする…といった感じでしょうか(そのため、条件分岐の際に self は省略しないほうがいいかなぁと思って追加したらドットを忘れていました。スミマセン(^_^;) あーっと。Ruby の作法だと #tied? とか #complete? というふうに名付けるべきなので self は省略してもそれっぽく読めますね…)。


…と書いていて気が付いたのですが、この方針にしたがうならば、Smalltalk のシンボルと違って、いちいち #to_s を噛まさないと #capitalize が使えない Ruby 1.8 のシンボルの場合、これもメソッド化して“隠して”しまったほうがいっそすっきりしますね(最初から頭を大文字にしたシンボル and/or 文字列を使え!…ちゅうか、そもそもハードコードするな!とかいう声も聞こえてきそうですが(^_^;))。


ということで、tied? とかもついでに直しつつ…

class TennisSetScorer
  def initialize; @games = {:player1 => 0, :player2 => 0} end

  def game_won_by(player); @games[player] += 1 end

  def leader; @games.index(leader_games) end
  def leader_name; leader.to_s.capitalize end
  def leader_games; @games.values.max end
  def opponent_games; @games.values.min end

  def complete?; leader_games == 7 or (leader_games == 6 && opponent_games < 5) end
  def tied?; leader_games == opponent_games end

  def state_str
    return "Set is tied at #{leader_games}" if tied?
    return "#{leader_name} wins the set #{leader_games} - #{opponent_games}" if complete?
    "#{leader_name} leads #{leader_games} - #{opponent_games}"
  end
end

class Array; def at_rand; self[rand(size)] end end

set = TennisSetScorer.new
until set.complete? do
  set.game_won_by([:player1, :player2].at_rand)
  puts set.state_str
end

これで #state_str を、条件分岐記述のみに簡素化できました。スンビャラスィ。