大は小を兼ねるジャンケンプログラムのアルゴリズムとは
この記事は、web制作者の自分が、プログラムをあまりしないフロントエンドエンジニアさん向けに「2進数・ビット演算・シフト演算の活用」について記事を作っている時に思いついた、大は小を兼ねる的なジャンケンプログラムのアルゴリズムをこっそり紹介してみようという内容です。
大学や専門学校の課題で複数人ジャンケンプログラムを作れ、というものの解答になってしまう可能性もありますので自分で考えたい人はネタばれ注意なので気を付けてください。
1対1のジャンケンを作ろうとして考えるのではなく、いきなりプレイヤーが何人いてもOKなジャンケンプログラムを作れば、1対1のジャンケンでも対応可能、という意味で「大は小を兼ねる」と表現しました。
ここで紹介するアルゴリズム(の計算式)なら、参加人数が10000人でも簡単に作れて対応可能、というものとなります。
(自分のサイトの記事では、5回のネタ記事の最後に応用的にプログラムで実戦、という例として紹介しています)
ジャンケンプログラムが紹介される背景
ジャンケンプログラムは「プログラム学習」のサンプルとして使われがちなために、「if~else」や「二次元配列」を教えるために、それを使った総当たり式(グー対グーであいこのような出しあっている手の判定)のアルゴリズムで紹介されがちな状況で、それ以上はあまり考察されたりしないのではないかと、ネット検索していると思ったりします。
[ if~else ] での総当たりチェックで、文字による比較(例:G と C と P など)を数値にして、それを配列インデックスと考えて「二次元配列で簡単に…」というシナリオも、初心者にとっては配列を学ぶための目から鱗的なストーリーなので、面白いのですが、それだけで終了してしまうと、5人で対戦とか10人で対戦というものを作ろうとすると、とても大変な作業になる、などと初心者は気付かないのではないかと思います。
if elseタイプの判定部分の例
※本題ではないので参考的なものです。
p : プレイヤーの手の値(0 or 1 or 2)
var c=Math.floor(Math.random()*3),
h=['G','C','P'],x=h[p],y=h[c],r='プレイヤーの';
if(x==='G'){ //プレイヤーがグーの時
if(y==='C'){r+='勝ち';}
else if(y==='P'){r+='負け';}
else{r='引き分け';}
}
else if(x==='C'){ //プレイヤーがチョキの時
if(y==='P'){r+='勝ち';}
else if(y==='G'){r+='負け';}
else{r='引き分け';}
}
else{ //プレイヤーがパーの時
if(y==='G'){r+='勝ち';}
else if(y==='C'){r+='負け';}
else{r='引き分け';}
}
//結果の r をinnerHTML変更等で画面表示する処理は省略
結果が「プレイヤーの勝ち」「プレイヤーの負け」「引き分け」のどれかになるような感じです。
配列タイプの判定部分の例
※本題ではないので参考的なものです。
p : プレイヤーの手の値(0 or 1 or 2)
上記の文字列比較の部分を数値にして配列インデックスにすると「配列で簡単に」の作り方になります。
var c=Math.floor(Math.random()*3),r,
k=[
['引き分け','あなたの勝ち','あなたの負け'],
['あなたの負け','引き分け','あなたの勝ち'],
['あなたの勝ち','あなたの負け','引き分け']
];
r=k[p][c];
//結果の r をinnerHTML変更等で画面表示する処理は省略
ですが、この作り方だと、5人の場合は 3*3*3*3*3 の数だけ総当たり判定となり、5次元配列で243個の勝・負・引を用意しなければいけなくなります。
また、何人で対戦するか不明、という状態だと、[if~else] や [多次元配列] での総当たり式アルゴリズムで作るのは判定結果の配列を用意するのが難しいかと思います。
誰かにプログラムを教えてあげよう、という教材としてのジャンケンプログラムであるならば、その続きとして思いつく複数人でのジャンケンの場合に、ここまでの総当たり式の作り方では難しい、ということを教えてあげてほしいのです。
そして別の案を考える時に、「プログラムがよくわからない」「それがどう使えるのかわからない」などと初心者をブロックしてしまいがちである「2進数・ビット演算・シフト演算」をスルーせずに調べたり使ってみたりすれば、こういうアイデアを思いついたりできるのです、という感じで教えてあげてほしいと思ったりします。
(多分)どんなプログラム学習でも教材として使える複数人対戦ジャンケンプログラムのアルゴリズム
javascriptで書けば、
k | = 1 << a ;
です。
初心者向けの記述なら
k = k | (1 << a) ;
でも可です。
kの初期値は 0
グーの時は a=0
チョキの時は a=1
パーの時は a=2
人数分ループさせてこの式で計算し、最後の k の値で勝敗が判定できます。
プレイヤーの数は10人でも10000人でもOKです。その分forループするだけです。
これは、ジャンケンの出ている手どうしを比較する、というのではなく、
『場にどんな手が出たかを順に k に計算してその値で判定する』という方法です。
※教材用として「2進数・ビット演算・シフト演算」を使うという
テーマにもなっています。
このアルゴリズムについては、プログラマの人なら
次の項目で例示する判定表で一目で意味が理解できるかと思います。
kの値による判定の表
ジャンケンは1対1でも100人でやっても、
場に出ているのが
グーとチョキだけなら、グーを出した人が勝ちです。
グー、チョキ、パーが出ていれば、引き分けです。
グーとパーだけしか出ていなければ、パーが勝ち。
全員がグーを出したらあいこで引き分け。
そういうルールなので、
場に何が出ているのか(数は関係ない)を『手が出ている』を 1 として表にして表現してみると、
以下のような判定表ができあがります。
表では、グーをG、チョキをC、パーをP、としています。
P | C | G | 十進数値 | 判定値 |
---|---|---|---|---|
0 | 0 | 1 | 1 | 全員Gで引きわけ |
0 | 1 | 0 | 2 | 全員Cで引きわけ |
0 | 1 | 1 | 3 | Gを出した人が勝ち |
1 | 0 | 0 | 4 | 全員Pで引きわけ |
1 | 0 | 1 | 5 | Pを出した人が勝ち |
1 | 1 | 0 | 6 | Cを出した人が勝ち |
1 | 1 | 1 | 7 | GCPで引きわけ |
この計算式を説明します。
表を見て二進数が表現されていることに気付けば、
プログラマなら後は簡単に思いつかれるかと考えます。
アルゴリズムとしている
[ k | = 1 << a ]
は、その計算式なのです。
初期の判定値が k=000(2進数)で、
グーが出れば、k | = 1<<0
つまり、000 | 001 = 001
k=001(2進数)= 1 (10進数) となります。
次がグーでも
001 | 001 = 001
なので k=001(2進数)= 1 (10進数) のままです。
(★ここで終了すれば、全員グーで引き分けです)
チョキが出ると、001 | 010 = 011 となります。
k= 011(2進数) = 3 (10進数)
(★ここで終了すれば、グーを出した人が勝ちとなります)
その次に
パーが出れば 011 | 100 = 111
k= 111(2進数) = 7 (10進数)
ここまでで、グーチョキパーで引き分けとなります。
kにビットマスクの値を足してもいいのですが、
その場合は同じ手の二回目以降は足さないという処理が面倒なので、
「ビットごとのOR ( | ) 」を使っています。
「ビットごとのOR ( | ) 」ならば同じ手のビットマスクを2回目以降計算させても
k の値は変化しないので安心です。
プログラム例(javascript)
p : プレイヤーの手の値(0 or 1 or 2)
n : 対戦するPCプレイヤーの数
※人間一人 対 100人PC なら n=100 です。
function zyanken_type_or(p,n){
var c=[],k=0,i,
x=['','全員Gで引きわけ','全員Cで引きわけ',
'Gが勝ち','全員Pで引きわけ','Pが勝ち','Cが勝ち',
'GCPで引きわけ'];
k|=1<<p; //プレイヤーの手を[|演算]
for(i=0;i<n;i++){ //pc n人分のループ
c[i]=Math.floor(Math.random()*3); //pcの手(0~2)
if(k!==7)k|=1<<c[i]; //pcの手を[|演算]
}
//後はx[k]で結果の文字列作成し画面表示したり、
//配列cや引数pから個別の勝ち負けを画面表示したり、
//好きなように作ります
}
上記では、k=7 となった後はkの値は変化しませんので、
[ if(k!==7) ] で判定してから処理しています。
※人間が複数なら、「プレイヤーの手を」の部分で
pを配列にしてforeach文にする、などが考えられます。
まとめ
大は小を兼ねるという意味は複数人の簡単なアルゴリズムで2人対戦のジャンケンにも使える上に、ifelse総当たりの配列を考える必要もなく、判定条件の7種類の値の処理だけでいいということになります。
5人対戦なら3*3*3*3*3=243個の
グー対グー対チョキ対パー対グーだとどうなる?
なんて五次元配列の値が間違ってないかを確認する必要もなく、
楽ができます。
ということで、
「2進数」「ビット演算のビットごとのOR ( | ) 」「シフト演算」を
活用すると、大は小を兼ねる複数人ジャンケンプログラムのアルゴリズムを
作ることができ、それは、
『場に出た一つの手につき【 k | = 1<<a 】の式で判定値を計算する』
という簡単なものになります。
プログラムの仕様は、
kの初期値 0
aの値を「グーの時 0 , チョキの時 1 , パーの時 2 ]
ならば、
z=[
'', // k=0 は手が出てない状態です
'全員Gで引きわけ',
'全員Cで引きわけ',
'Gを出した人が勝ち',
'全員Pで引きわけ',
'Pを出した人が勝ち',
'Cを出した人が勝ち',
'GCPで引きわけ'
];
の配列インデックスとして、
z[k]
で結果文字列が用意できる
備考
もちろん、上記の判定条件の表とその計算のアルゴリズムだとわかった状態であれば、
プログラムのループ中で、何度もシフト演算させずに、
t=[ 1<<0 , 1<<1 , 1<<2 ]; → t=[1,2,4];と用意し、
グーの時配列インデックス番号 n=0
チョキの時配列インデックス番号 n=1
パーの時配列インデックス番号 n=2
k | = t[n]
などと配列値で計算部分を改良しても良いと思います。
この記事ではアルゴリズムの式の中で
「2進数」と「ビット演算の | 」と「シフト演算」の知識がポイントとなるので、
シフト演算でビットマスクを用意している部分を表現するために
サンプルでもそれをそのまま計算させて使っています。
※デザイナーさんやフロントエンドエンジニアさん向けに
「2進数の活用」から説明している記事は
下記サイトに別の文章展開で書いてますのでよかったら、ご参照ください。