じゃんけん勝敗判定アルゴリズムの思い出
昔、中学生の頃、BASICでプログラミングの勉強をしていて、何の本かは忘れてしまったんですが、たった1行でじゃんけんの勝敗判定ができる計算式というのが載っていました。「たった1行で勝敗判定ができるなんてすげー!」と思ったものです。どんな計算式だったかは正確には覚えていないのですが、とても短い式でした。
やってる内容もよくわからないままその式を組み込んでじゃんけんゲームを作りました。キーボードからグー・チョキ・パーに対応する数字を入力して、コンピュータには乱数発生させて、その式で勝敗判定、結果を表示するというだけのものでしたが。「なんでこれで判定できるんだろう?」って不思議に思ったことを覚えています。僕にとって「プログラミングって面白いなあ」って思った出来事のうちのひとつでして、未だに思い出すことがよくあります。
ただ、じゃんけんの判定なんてものはそうそう使う機会はないので、ふと「そういえばあのじゃんけんの式、どんなんだっけな…」と思い出してはその都度考えて作る、ということをこれまでに何度かしています。今回も思い出しがてら作ってみようと思います。これから作るのは当時見たものと完全に同じものというわけではないですが(言語もBASICでしたし)、大筋では同じものになるはずです。
とりあえず、ごく普通に作ってみる
後で不都合起きるかもしれませんが、暫定的にグーを 0、チョキを 1、パーを 2 で表すことにします。
そもそもじゃんけんなんてものは、グー、チョキ、パーの3通りしかないので勝負の組み合わせも9通りしかなく、if文なりswitch文なりで総当りで書いたところで大した手間ではないんですが。まずはその総当りで判定する関数を作ってみます。
Aさん(a)とBさん(b)それぞれの手を引数に、グー・チョキ・バーを0〜2で与え、Aさんから見て勝ったのか(WIN)、負けたのか(LOSE)、それともあいこ(DRAW)だったのかを判定します。エラーチェックは用意してないので、必ず引数として 0〜2の値が与えられるようにする必要があります。JavaScriptです。他の言語の場合は適宜読み替えてください。
function checkJanken(a, b) { if (a == 0) { // A:グー if (b == 0) { // B:グー return "DRAW"; } else if (b == 1) { // B:チョキ return "WIN"; } else { // B:パー return "LOSE"; } } else if (a == 1) { // A:チョキ if (b == 0) { // B:グー return "LOSE"; } else if (b == 1) { // B:チョキ return "DRAW"; } else { // B:パー return "WIN"; } } else { // A:パー if (b == 0) { // B:グー return "WIN"; } else if (b == 1) { // B:チョキ return "LOSE"; } else { // B:パー return "DRAW"; } } }
そもそもいくらif文だけで書くといっても、こんなに冗長に書く必要はないんですが。わかりやすさ優先ということで。処理速度も微々たるものだし、可読性も良いのでぶっちゃけこれで充分です。
見た目カッコよくはないけど要件は満たしてるし、わざわざわけのわからん計算式作って1行にする労力のほうがもったいない気がしてくるんですけど、趣旨が変わってしまうので続けます(笑) 誰も読んでくれなくても書き続けます!
法則性を探してみる
とりあえず、Aさんは出す手は固定で、Bさんがグー、チョキ、パーの順で出す手を変えていった時に勝敗がどうなるかを表にしてみようと思います。本来3回で充分ですが、法則性をつかむため3周、9回勝負します。
勝敗表 | Bさん | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
グー(0) | チョキ(1) | パー(2) | グー(0) | チョキ(1) | パー(2) | グー(0) | チョキ(1) | パー(2) | ||
A さ ん | グー(0) | あいこ | 勝ち | 負け | あいこ | 勝ち | 負け | あいこ | 勝ち | 負け |
チョキ(1) | 負け | あいこ | 勝ち | 負け | あいこ | 勝ち | 負け | あいこ | 勝ち | |
パー(2) | 勝ち | 負け | あいこ | 勝ち | 負け | あいこ | 勝ち | 負け | あいこ |
勝ちは青、負けは赤、あいこはグレーです。どの行も、あいこ→勝ち→負け→あいこ→勝ち→負け…という風に、規則的に並んでいることがわかりますね。
次は、計算式をどうしていくかですが、まず、手の種類に関係なく、2人が同じ手を出したら確実にあいこなわけですから、引き算するのが都合良さそうです。
あいこ
} else {
勝ちか負けのどっちか
}
というわけです。そこで、上記の表の勝敗結果に a - b の値を追加します。
a - b | Bさん | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
グー(0) | チョキ(1) | パー(2) | グー(0) | チョキ(1) | パー(2) | グー(0) | チョキ(1) | パー(2) | ||
A さ ん | グー(0) | 0 | -1 | -2 | 0 | -1 | -2 | 0 | -1 | -2 |
チョキ(1) | 1 | 0 | -1 | 1 | 0 | -1 | 1 | 0 | -1 | |
パー(2) | 2 | 1 | 0 | 2 | 1 | 0 | 2 | 1 | 0 |
-2〜2の間で数値が変動していて、同じ勝ち(負け)でも2種類の数字があるようです。一見、法則性がないように思えますが、よく見ると以下のようになっています。
あいこ … 必ず 0
勝ち …… -1 または 2
負け …… -2 または 1
ここまで来ればもう、ほとんど完成したようなものですね。「-1 か 2 が返ってきたら勝ち!」ってやってしまえば簡単ですが、本来勝ち・負け・あいこの3つしかないはずの勝敗結果の値が 5 つもあるのはスマートじゃないですので、最後に一手間加えます。
完成!
勝ちの -1 と 2、負けの -2 と 1 は、どちらも 3 離れてます。2 - (-1) = 3、1 - (-2) = 3 です。ということは、a - b の答えに 3を足して正の数にスライドさせ、その数の 3 の剰余(3で割ったあまり)を出せば、3通りに絞れるはずです。計算式は、(a - b + 3) % 3 ということになります。ちなみに '%' は剰余、割り算した時のあまり、という意味です。
例:7 % 3 = 1 (7 ÷ 3 = 2 あまり 1)
大抵の場合、剰余は %記号を使いますが、言語によって書き方が違う場合がありますのでご注意ください。例えば C/C++, Java, JavaScript, PHP, Rubyなどでは % を使いますが、PascalやVisual Basicでは mod と記述します。7 mod 3 みたいな感じです。今回は単なる計算式を作っているだけですので特に言語は意識していませんが、以下、% を使用します。
(a - b + 3) % 3の結果を表にしてみましょう。
(a-b+3)%3 | Bさん | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
グー(0) | チョキ(1) | パー(2) | グー(0) | チョキ(1) | パー(2) | グー(0) | チョキ(1) | パー(2) | ||
A さ ん | グー(0) | 0 | 2 | 1 | 0 | 2 | 1 | 0 | 2 | 1 |
チョキ(1) | 1 | 0 | 2 | 1 | 0 | 2 | 1 | 0 | 2 | |
パー(2) | 2 | 1 | 0 | 2 | 1 | 0 | 2 | 1 | 0 |
勝ちは 2、負けは 1、あいこは 0、と3通りの結果にまとめることができました。
もう3回繰り返す必要はないので縮めて、表の完成です。
(a-b+3)%3 | Bさん | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
グー(0) | チョキ(1) | パー(2) | ||||||||
A さ ん | グー(0) | あいこ(0) | 勝ち(2) | 負け(1) | ||||||
チョキ(1) | 負け(1) | あいこ(0) | 勝ち(2) | |||||||
パー(2) | 勝ち(2) | 負け(1) | あいこ(0) |
使ってみる
あとは使う言語に合わせて関数化しておけばじゃんけん勝敗判定がいつでも使えるようになりますね!(だから、めったに使わないって)
JavaScriptの関数を掲載しておきます。計算式の結果だけreturnして表示なりなんなりは呼び出し側でやってしまう方が好きなんですが、先ほどの関数と互換性を持たせて文字列で結果を返すことにします。言語によって先ほどの剰余の演算子の違いや、if文の書き方とか文法的な違いはあると思いますが、適宜読み替えてください。
function checkJanken(a, b) { var c = (a - b + 3) % 3; if (c == 0) { return "DRAW"; } else if (c == 2) { return "WIN"; } else { return "LOSE"; } }
こっちは勝敗結果だけなら1行で済むというメリットはありますが、後で見直した時に、せめて上の表とかコメントに書いておかないと、正直何やってるかよくわからなくなると思います。それなら上のほうで載せた、if文だけで総当たりでやっちゃう方が可読性も高いですしね。そういう意味ではこの計算式を使った勝敗判定はちょっとよろしくないとは思います。ただ、今回はじゃんけんなのでアレですが、モノによっては人間にとっての読みやすさを犠牲にすることでパフォーマンス向上が見込める場合もあると思いますので、一概にはいえないですが。
まあ、「if文だけ使って書いた関数も、この計算式使って書いた関数も、同じ結果を返すなんて!」というところにあの日、中学生だった僕は感動したわけですし、定石はあるにせよプログラミングというものはいろいろな書き方ができる、そこが楽しいってことなんじゃないかな、と思います。
そんなわけで、完全に自己満足ではありましたが、思い出の「じゃんけん勝敗判定アルゴリズム」復刻完了です!