じゃんけんの勝ち負け判定を Squeak Smalltalk で


arton師の ご指摘(…というよりは、あからさまな煽り?w)をうけて、「なぜ、あなたはJavaでオブジェクト指向開発ができないのか」の SqueakSmalltalk への移植を暇をみて書いてみてはいるものの、遅々として作業が進まない理由の一つに、ひとつひとつは比較的短いコードであるにもかかわらず、おどろくほど頻繁に、俺なら〜善し悪しは別として〜ぜったいこうは書かない!(けど、移植だからあんまり勝手な表現変更はできない…)というジレンマに再三さいなまれ、そのたびにやる気がそがれているから…汗、ということがあります。

そうした事例の代表格が、じゃんけんの勝ち負け判定のくだり。くだんの書籍では IF 文で全条件を列挙して OR でつないで表現しているのですが、これって面倒くさがり屋の私にはまっさきに“無いよな…”と思われる書き方そのものずばりでして、判定のところにさしかかるたびにブルーな気分になってけっきょくそこでくじけてしまっていたわけです。


まあ、泣き言はおいておくとして、くしくもそんな折、東方算程譚 - オブジェクトじゃんけん 経由で、こんなお題を見かけましたので、これまで頭の中でぼんやりとイメージしていた判定方法で実際にも対応可能か確かめるべく、さっそく書いて試してしてみることに。


このお題の要件は「グー」「チョキ」「パー」を別のものに差し替えても勝負が成立するようにすることですが、まずは「グー」「チョキ」「パー」だけで書いてみます。あと簡単のため、出す手は双方ともランダムにしました。

World findATranscript: nil.
10 timesRepeat: [

    | 手の種類 Aの手 Bの手 判定結果 |

    手の種類 := #(グー チョキ パー).
    Aの手 := 手の種類 atRandom.
    Bの手 := 手の種類 atRandom.

    判定結果 := (手の種類 indexOf: Aの手) - (手の種類 indexOf: Bの手) \\ 3 caseOf: {
        [2] -> ['Aの勝ち'].
        [1] -> ['Bの勝ち'].
        [0] -> ['あいこ']}.

    Transcript cr; show: ('A「{1}」 vs B「{2}」で、{3}' format: {Aの手. Bの手. 判定結果})
]
A「チョキ」 vs B「チョキ」で、あいこ
A「グー」 vs B「チョキ」で、Aの勝ち
A「パー」 vs B「チョキ」で、Bの勝ち
A「チョキ」 vs B「チョキ」で、あいこ
A「チョキ」 vs B「グー」で、Bの勝ち
A「パー」 vs B「チョキ」で、Bの勝ち
A「グー」 vs B「パー」で、Bの勝ち
A「パー」 vs B「パー」で、あいこ
A「チョキ」 vs B「グー」で、Bの勝ち
A「パー」 vs B「グー」で、Aの勝ち


大丈夫みたいですね。勝敗は手の隔たりを 3 で割った余りで判断しています。まあ、コードを読む側としては、「なぜ、あなたはJavaで〜」での書き方のように、いかにも愚直に勝ち負け判定条件が列挙されているのを見ることで、一発で何をしたいのか予備知識無しでもわかる…という“メリット”もなくはないので、ここで示した、簡素だが若干トリッキーな要素を持つ書き方と「なぜ、あなたはJavaで〜」での書き方と比較した場合、ある種のトレードオフも多少は生じうるかとは思います。


上のコードを一部改編して、お題の本題である、複数の“手の組み合わせ”にも対応しましょう。

World findATranscript: nil.
10 timesRepeat: [

    | 手の種類群 Aの手 Bの手 手の数値化 判定結果 |

    手の種類群 := #((グー チョキ パー) (カエル へび ナメクジ) (石 はさみ 紙)).
    Aの手 := 手の種類群 atRandom atRandom.
    Bの手 := 手の種類群 atRandom atRandom.

    手の数値化 := [:手 | (手の種類群 detect: [:手の種類 | 手の種類 includes: 手]) indexOf: 手].

    判定結果 := (手の数値化 value: Aの手) - (手の数値化 value: Bの手) \\ 3 caseOf: {
        [2] -> ['Aの勝ち'].
        [1] -> ['Bの勝ち'].
        [0] -> ['あいこ']}.

    Transcript cr; show: ('A「{1}」 vs B「{2}」で、{3}' format: {Aの手. Bの手. 判定結果})
]
A「パー」 vs B「石」で、Aの勝ち
A「グー」 vs B「チョキ」で、Aの勝ち
A「グー」 vs B「カエル」で、あいこ
A「チョキ」 vs B「紙」で、Aの勝ち
A「チョキ」 vs B「ナメクジ」で、Aの勝ち
A「カエル」 vs B「はさみ」で、Aの勝ち
A「グー」 vs B「ナメクジ」で、Bの勝ち
A「チョキ」 vs B「石」で、Bの勝ち
A「チョキ」 vs B「グー」で、Bの勝ち


余談ですが、#detect: するとき、#includes: の時点で、#indexOf: で得られる情報は内部的に分かっているはずなので、それを再利用する…ということを素直に表現できないところが、ひごろ Smalltalk を使っていてちょくちょく感じる“もどかしさ”だったりします。もっとも、こうした不満は、なにも Smalltalk 特有のものというわけではなく、たいていのプログラミング言語でも同じことを感じてよいはずなのですが、他の言語だと Smalltalk にはないもっと別の次元の不満が爆発するので、結果、気づくのは Smalltalk で気分よくw書いているときだけ…ということみたいです。


ついでに、三名以上の対戦にも対応してみます。

World findATranscript: nil.
10 timesRepeat: [

    | 手の種類群 参加者数 場の手たち 手の数値化 場の手の種類 勝者列挙 判定結果 |

    手の種類群 := #((グー チョキ パー) (カエル へび ナメクジ) (石 はさみ 紙)).
    参加者数 := 3.
    場の手たち := Array streamContents: [:ストリーム |
        参加者数 timesRepeat: [ストリーム nextPut: 手の種類群 atRandom atRandom]].

    手の数値化 := [:手 | (手の種類群 detect: [:手の種類 | 手の種類 includes: 手]) indexOf: 手].
    場の手の種類 := (場の手たち collect: [:各々 | 手の数値化 value: 各々]) asSet asArray.
    勝者列挙 := [:セレクタ |
        場の手たち select: [:各々 | (手の数値化 value: 各々) = (場の手の種類 perform: セレクタ)]].

    判定結果 := 場の手の種類 size = 2 ifFalse: ['あいこ'] ifTrue: [
        場の手の種類 first - 場の手の種類 second \\ 3 caseOf: {
            [2] -> [(勝者列挙 value: #first) printString, 'の勝ち'].
            [1] -> [(勝者列挙 value: #second) printString, 'の勝ち'].
            [0] -> ['あいこ']}].

    Transcript cr; show: ('{1} の対戦で、{2}' format: {場の手たち. 判定結果})
]
#(#はさみ #紙 #チョキ) の対戦で、#(#はさみ #チョキ)の勝ち
#(#グー #カエル #ナメクジ) の対戦で、#(#ナメクジ)の勝ち
#(#ナメクジ #はさみ #パー) の対戦で、#(#はさみ)の勝ち
#(#石 #カエル #ナメクジ) の対戦で、#(#ナメクジ)の勝ち
#(#へび #紙 #パー) の対戦で、#(#へび)の勝ち
#(#紙 #紙 #ナメクジ) の対戦で、あいこ
#(#カエル #はさみ #へび) の対戦で、#(#カエル)の勝ち
#(#へび #カエル #はさみ) の対戦で、#(#カエル)の勝ち
#(#へび #石 #チョキ) の対戦で、#(#石)の勝ち
#(#紙 #石 #カエル) の対戦で、#(#紙)の勝ち


例によって Smalltalk だけだと誰も読んでくれなさそうなので汗、二番目の二人対戦版を Ruby で直訳っぽくしたものも書いておきます。

10.times{

  手の種類群 = [[:グー, :チョキ, :パー], [:カエル, :へび, :ナメクジ], [:石, :はさみ, :紙]]
  Aの手 = 手の種類群.choice.choice
  Bの手 = 手の種類群.choice.choice

  手の数値化 = proc{ || 手の種類群.detect{ |手の種類| 手の種類.include?(手) }.index(手) }

  判定結果 = case (手の数値化.call(Aの手) - 手の数値化.call(Bの手)) % 3
    when 2; 'Aの勝ち'
    when 1; 'Bの勝ち'
    when 0; 'あいこ'
    end

  puts "A「#{Aの手}」 vs B「#{Bの手}」で、#{判定結果}"
}


#choice の無いバージョンでエラーになる場合は、次のように定義しておけばOKです。

class Array
  def choice
    self[rand(size)]
  end
end