いきなり結論のコード:
#include <time.h>
int printf(const char*,...);int scanf(const char*,...);char a[3],g[4],z=48;int
rand();void srand(unsigned);int f(char*x){return x[0]==x[1]||x[0]==x[2]||x[1]
==x[2];}int i,h,b;int main(){srand(time(NULL));do{a[0]=z+rand()%9+1;a[1]=z+ra\
nd()%10;a[2]=z+rand()%10;}while(f(a));do{h=0;do printf(">"),scanf("%3s",g);wh\
ile(f(g));for(i=0;i<3;i++)h+=a[i]==g[i]?g[i]=0,1:0;b=0;for(i=0;i<9;i++)b+=a[i/
3]==g[i%3]?1:0;printf("H%d B%d\n",h,b);}while(h!=3);printf("OK!\n");return 0;}
はじめに
小学生のときに、紙と鉛筆だけでできる対戦ゲームが大流行したことがありました。今思うと 数当てゲーム MOOを 3 桁にした変形版なんだろうと思います。マスターマインドの変形版とも言えますが、当時の僕らは H がいくつで B がいくつ、といったフォーマットで情報をやりとりしていたので、ここではヒット&ブローという呼び名で統一したいと思います(正確に言えば、3 桁の Hit & Blow ということになるかとは思いますが、それはそれとして)。
今回は、コンピュータが考えた答えをプレイヤーがひたすら推測し続ける、というプログラムとなります(対戦ゲームではありません)。
ゲームのルール
準備
小学生の僕たちが遊んでいたこのゲームは、2 人で遊ぶゲームでした。各自、最初に答えとなる 3 桁の数字を、相手に見えないようにメモします。この 3 桁の数字は何でも良いというわけではありません。各桁は全て異なる数字でなければいけません。つまり、111 とか 565 などの数字は、答えの数字にはしてはダメです。もうひとつのルールとしては、最初の桁は 0 にしない、というものがありました。MOO の説明をみると、最初の桁が 0 であっても良いみたいなのですが、小学生だったので、012 は 12 で 2 桁じゃん!という考えがあったのではないかと思われます(なので、ここでは回答となる数字は少なくとも 102〜987 の範囲にある整数、ということになります)。
Hit と Blow
いわゆる Hit & Blow と同じですが、念のため、ここでも説明しておきます。Hit は場所も数字も一致しているものの数を表します。例えば、987 という解に対して、907 と問い合わせる(攻撃した)場合、3 桁目の 9 と 1桁目の 7 が数字の種類と位置が合っているので、Hit は 2 となります(9 と 7 の 2 つの数字が Hit した、という意味です)。これを単に H2 と表記することとします。
場所は合っていないけど、数字の種類が合っているものを Blow とします。Hit が命中なら、Blow はかすったという意味でしょうか。先の例で言うと、987 に対し、670 と問い合わせた場合、2 桁目の 7 は位置が異なっているけれども、種類は合っているので Blow となります。そしてそのような数はこの 7 だけなので、Blow は 1 となります。これを単に B1 と表記することにします。
もちろん、987 に対し、123 という問い合わせをすると、Hit も Blow もひとつも無いので、H0 B0 という結果となります。このような結果が得られると、9 つの数字のうち、問い合わせに使った数字は全く関係ないこととなりますので、非常に大きなヒントとなります(今の例では、123 は解に存在しないので、4〜9 の 6 桁が解の候補となるわけです)。
H と B の数を用いて、このゲームの終了条件を書くと、H3 となる数を入力するまでゲームが続くこととなります。なお、ネットの記事を色々みてみると、H3 とは書かずに 3H と数字が先にくる書き方の方が主流のようですが、ここでは私の小学生時代のルールに倣い、数値を後に示す流儀でいきます。
清書版のコードと、その解説
// gcc spoiler.c
#include <time.h>
int printf(const char*,...);
int scanf(const char*,...);
int rand();
void srand(unsigned);
char a[3],g[4],z=48; // 48=='0'
int i,h,b; // h は Hit 数、b は Blow 数
// 同じ数字があったら true を返す
int f(char*x) {
return x[0]==x[1] || x[0]==x[2] || x[1]==x[2];
}
int main() {
srand(time(NULL));
// 問題を作る
do {
a[0]=z+rand()%9+1;
a[1]=z+rand()%10;
a[2]=z+rand()%10;
} while(f(a));
do {
do
printf(">"), scanf("%3s",g); // カンマ式による短縮
while(f(g));
// Hit 数計算
h=0;
for(i=0;i<3;i++)
h += a[i]==g[i] ? g[i]=0,1 : 0;
// Blow 数計算
b=0;
for(i=0;i<9;i++)
b += a[i/3] == g[i%3] ? 1 : 0;
printf("H%d B%d\n",h,b);
} while(h!=3);
printf("OK!\n");
return 0;
}
コンピュータが想定した値は、配列 a に文字コードで格納します(実際は ASCII コード)。プレイヤーが入力した値は配列 g に格納されています。i=0,1,2 の全てに対して a[i]==g[i] が成立したらゲーム終了です。
Hit 数計算部分
Hit 数計算部分では、カンマ式を使って Hit 数計算と、Hit した数が次の Blow 計算で再度カウントされないようにしています(以下の部分です)。
h += a[i]==g[i] ? g[i]=0,1 : 0;
もし、a[i]==g[i] が成立しているならば(つまり位置も数字も合っているならば)、g[i]=0,1 が実行されます。カンマ式のルールにより、g[i]=0 が実行され、その後 1 が評価されて、3 項演算子の評価結果は 1 となり、最終的には h+=1 が実行されることになります。
また、g[i]=0 としているため、次の Blow 数計算部分でも g[i] は Blow している入力としては評価されません(g には文字コードが入っているので、0 としておけば、'0'〜'9' の値(文字コード)とは異なる状況となります。
Blow 数計算部分
Blow 数計算部分は、
// Blow 数計算
b=0;
for(i=0;i<9;i++)
b += a[i/3] == g[i%3] ? 1 : 0;
です。
i を 0〜8 まで回していますが、配列 a に対しては i/3 を、g に対しては g[i%3] としているので、結果として a[0],a[1],a[2] と b[0],b[1],b[2] の全てのペアを比較することとなります:
a[0] と b[0] // i=0 のとき(以下同様)
a[0] と b[1] // i=1
a[0] と b[2] // i=2
a[1] と b[0] // i=3
a[1] と b[1] // i=4
a[1] と b[2] // i=5
a[2] と b[0] // i=6
a[2] と b[1] // i=7
a[2] と b[2] // i=8
その他
ヘッダのインクルードを減らすため、自前でプロトタイプ宣言を書いたり、また、カンマ式を活用したりしていますが、基本的にはそんなにトリッキーなことはしていません。プログラムとしては結構素直な実行なのではないかな、と思っております。
ゲームの様子:
$ ./a.out
>123
H1 B0
>145
H1 B0
>167
H0 B0
>126
H0 B0
>146
H1 B0
>843
H2 B0
>943
H3 B0
OK!
この時の答えは 943 でした。