みなくんの日記

やる気が皆無で自由気ままに生きてる人のブログ

プログラムよ、とにかく動け ~4桁のヒット&ブローゲーム「4Numbers」を作ろう~その3

更新が大幅に遅れました。私がサボっていたせいです。許して。

今回の目標

  • [Chapter.3] 正解桁との比較処理の実装

[Chapter.3]正解桁との比較処理

前回までの処理で「ユーザが正解だと思う4桁の数字」を取得できるようになったところで、Chapter.1で実装した正解の4桁との比較処理を行いましょう。
あれこれ説明しようかと悩んだのですが、これは実際にコードを見てもらって解説を加えた方が読みやすいかなと思ったのでいきなりコードを載せたいと思います。

class compare_number:
    def cmp_num(self,number,answer):
        hit = 0
        blow = 0 #ヒット・ブロー数
        if(answer == number): #完全ヒットであった場合
            print("正解です!")
            return True #ループ終了の合図
        else: #違う場合
            for i in range(4):
                if(answer[i] == number[i]): #まず部分ヒットかどうか調べる
                    hit += 1
                else: #部分ヒットではなかった場合
                    j=0
                    while(j<4):
                        if(i == j): #部分ヒット判定済箇所は除外するためjをカウントアップ
                            j += 1
                        if(j == 4): #カウントアップによるセグメンテーションフォルトの回避
                            break
                        if(answer[i] == number[j]):#ブローを発見した場合
                            blow += 1
                            break
                        j += 1
        print("HIT=", hit,"BLOW=",blow) #結果表示
        return False #ループ継続の合図

プログラムの流れを簡単に示します。

  • hit=ヒット数,blow=ブロー数として変数を設定
  • 以下に示す手順に従って判定処理を行う
    (1) 正解であった場合
     - 「正解です!」と表示してゲーム終了フラグを「True(終了)」として処理終了
    (2) 不正解であった場合
     - 条件(1桁目が部分ヒットであるか)→部分ヒットの場合は変数hitをカウントアップして2桁目を見る
     - 1桁目が部分ヒットではない→whileループによるブロー処理開始
     - ブロー処理が終了→ヒット数とブロー数を表示、ゲーム終了フラグを「False(継続)」として処理終了

流れとしては実に簡単ではありますが、もう少し具体的な処理についてみていくことにしましょう。

ヒット・ブローをどうやって判定していくか

正解?不正解?

そもそもユーザが導き出した答えが正解かどうか、これはもちろん最初に判断すべきことだと思います。コードでは以下の部分になりますね。

if(answer == number): #完全ヒットであった場合
    print("正解です!")
    return True #ループ終了の合図

answer=機械が出した数字、number=ユーザが出した数字 になっています。つまりこの条件は「機械が出した答え=ユーザが出した答え(完全一致)」となる時に処理される内容なんですね。
return文で「True」を返り値としています。これは設計図に書いた「3.正解桁との比較」から「2.ユーザが任意の4桁を入力」に戻る(ループする)ために利用する返り値で

  • Trueならループしない(ゲーム終了)
  • Falseならループする(ゲーム継続)

という仕様になっています。(これは次回のプログラミング講座「メイン関数を作ってゲームを完成させよう!」で使う値になります)
ちなみに不正解であれば完全一致とはならないので、この条件内の処理は行われません。

不正解→部分ヒット?ブロー?

機械が出した答えと違った時、このゲームではヒントとして「部分ヒット数=2、ブロー数=1」のような情報を提示するため、その判定処理が必要になります。一番今回で難しいところはここでしょうね。今回実装したアルゴリズムもまだ改善できるのかな…などまだ悩みどころは多いのですが、とりあえず今回は動いているので良しとします。
ブロー処理の具体的な処理の流れを、実例とコードを用いて説明していきましょう。見ただけで大体分かるわ、って人はここから少し長いので次の章へ進んでください。

(例) 正解=1234でユーザの答え=2537の場合(HIT=1 BLOW=1)
(1) 1桁目の比較(正解 "1" ユーザ "2")→部分ヒットではないのでブローがあるか調べる(if文の条件が満たされないためif文中処理は実行されない)
コード(answer[i]=1,number[i]=2,i=0)

for i in range(4): # i=0,1,2,3の順で増加
    if(answer[i] == number[i]): #まず部分ヒットがあるかどうか調べる(条件を今回は満たさない)
        hit += 1 #この処理は行われない!

(2) 正解1桁目(桁変数は"i")とユーザ1桁目(桁変数は"j")のブロー探索→既にi=j=0のパターンは部分ヒットの際に比較済みなのでユーザ2桁目から探索することにする(jをインクリメントする)
(i=0,j=0 "j"はユーザが答えた数字を格納する配列number[]に用いる変数)

j=0
while(j<4): 
    if(i == j): #部分ヒット判定済箇所は除外するためjをカウントアップ
        j += 1

(3) 1桁目「1」と2桁目「5」の比較→ブローではない→if文「ブローを発見した場合」の処理は行わず、ユーザ3桁目の比較に移る(jをインクリメント)

if(answer[i] == number[j]):#ブローを発見した場合
    blow += 1
    break
j += 1 #←この処理だけを行う!

(4) (3)を最後まで繰り返す→ブローは存在しなかった

(5) 正解の2桁目の比較(answer[i]=2 number[i]=5 i=1)→部分ヒットではないのでブローがあるか調べる
(6) i=1,j=0のとき…answer[i]=2 number[j]=2となり条件を満たす!→blowをインクリメントする

if(answer[i] == number[j]):#ブローを発見した!
    blow += 1 #blow=1にする
    break #これ以上他の桁と比較する必要はないのでjのwhileループを抜ける
j += 1 

(7) これ以上他の桁と比較する必要はないのでjのwhileループを抜け、正解の3桁目の比較へ移る
(8) 正解の3桁目の比較(answer[i]=3 number[i]=3 i=2)→部分ヒットなのでhitをインクリメント
(9) 部分ヒットしたためブローチェックの必要なし。次の桁の比較に移る
(10) 正解の4桁目の比較→部分ヒットなし→ブローチェック→ブローなし
(11) ヒット数とブロー数を表示、返り値をFalseとして処理終了

ちなみに、変数"i"のforループで正解桁を動かし、変数"j"のwhileループでユーザ桁を動かすようにすることで、以下の動きを実現しています。

  • whileループを抜ける→ブローチェックの終了
  • forループを抜ける→正解桁との比較終了

また、変数"j"の値をforループではなくwhileループにしているのには訳があります。
変数"j"のforループ中に変数"j"の値を累計代入しても、ループ時に変数"j"の値は変更されるという仕様が存在したためです。この仕様が適用されていると、変数"j"をインクリメントしても次のforループで値が上書きされてしまうので、比較処理ができなくなるんです。だからwhileループを用いたわけです。

言葉じゃ見にくいわ!って人へ

先ほどの(1)~(11)までの動きが説明が下手だから分からない…という人はこちらの図を参考にしてみてください。
f:id:minamint:20180418225825p:plain

プログラムの実行結果

最初に提示したプログラムの下に以下を加えて実行してみます。

#---test seciton---

hoge=compare_number()
print(hoge.cmp_num("2537","1234")) #hit=1,blow=1
print(hoge.cmp_num("1235","1234")) #hit=3,blow=0
print(hoge.cmp_num("4321","1234")) #hit=0,blow=4
print(hoge.cmp_num("1234","1234")) #hit=4

#---test end----

実行結果は以下の通り。

HIT= 1 BLOW= 1
False
HIT= 3 BLOW= 0
False
HIT= 0 BLOW= 4
False
正解です!
True

正しく動作していることが分かりますね。また、hit数とblow数が重複していないということも確認できます。でも少し心残りですね…例題ではユーザが真面目な方なので、重複しない4桁の数字を入力してくれているのですが…。

「1111」がユーザの解答だった時、プログラムはどう動く?

テスト実行だと私が期待する値を入力値に設定できるのですが…実際に他の人に動かしてもらう際、そのユーザは私の期待通りに動いてくれるとは限りません(別に信頼してないわけじゃないですよ!)。そのため、予想される入力値とは全然違った値が入力されることもあります。
「-123」や「aaaa」などは前回の入力処理で受け付けないようにできたのですが、まだもうひとパターンだけ怪しいものがあります。
それは「1111」といったゾロ目や「1212」といった数字が重複している4桁の場合。
恐らく前回のプログラムを読んだ方は疑問に思っていたのではないでしょうか。
で、この重複パターンを入力した際に今回作成した比較プログラムがどういった動きをするのか…少し考えてみましょう。
とはいっても考えるより行動した方が早い、ということでtest sectionを以下のように書き換えて実行してみましょう!

#---test seciton---

hoge=compare_number()
print(hoge.cmp_num("2525","1234")) #hit=0,blow=1 2がblow、しかも2が重複している。
print(hoge.cmp_num("1212","1234")) #hit=2,blow=0 "12"がhitだが、後ろの12もblowになってしまう…?
print(hoge.cmp_num("4422","1234")) #hit=0,blow=2 4と2がblow、でも両方重複している。2と4が解答にあると分かってれば使える位置特定手段。
print(hoge.cmp_num("2244","1234")) #hit=2,blow=0 さっきの逆。これで2と4がどちらにあるか分かるが、大まかにしか分からないのであまり良い手段とも言えない。
print(hoge.cmp_num("1111","1234")) #hit=1,blow=0 全部1。その桁が正解に含まれるかどうかを調べるのに使える戦法だったりする。ズルいけどアリ。

#---test end----

気になる実行結果はこちら…

HIT= 0 BLOW= 1
False
HIT= 2 BLOW= 0
False
HIT= 0 BLOW= 2
False
HIT= 2 BLOW= 0
False
HIT= 1 BLOW= 0
False

なんと!予想通りの結果になっている…!
それもそのはず。処理中に行った以下の動作が、重複カウントを防いでくれてたんですね。
* 最初に部分ヒットか調べる→ヒットしていれば次の正解桁の比較を開始してブロー処理を行わないので、正解桁以外にその数字が書いてあっても判定されない * ブローを発見したら次の正解桁との比較に移る→ブローの重複カウントが行われない * 部分ヒットで調べたユーザ桁はインクリメントでスキップ→ヒット・ブローの重複カウントが行われない 実装の際はここまでの配慮をしないとユーザの動きに対応できないんですね…簡単そうなゲームなのに、実は難しかったわけだ…。

まとめ

  • 正解桁との比較処理の実装ができた
  • 重複した数字との比較でも正しい動作をすることが確認できた

次回で最後となります、メイン関数の実装・プログラムの完成編です!

補足(whileとforループの使い分けについて)

変数"j"のforループ中に変数"j"の値を累計代入しても、ループ時に変数"j"の値は変更されるという仕様について、実際にどうなってしまうのかをインタプリタ上でテストして検証してみます。

>>> for i in range(4):
...     print("for:",i)
...     i=i+2
...     print("i+2=",i)
...     print(" ")
...

実行結果はこちら。

for: 0
i+2= 2

for: 1
i+2= 3

for: 2
i+2= 4

for: 3
i+2= 5

といった具合に、(forでカウントされた値)+2になっている&forループの値が演算後の値になっていないことが分かります。恐らくループする度に変数"i"にrange()の値が代入されていくから演算後の値が上書きされているのでは…。
これのせいで、今回の処理だと変数"j"がforループの時に正しく処理してくれないというわけです。よって、whileループを今回は用いることにしました。