女神転生のコードブレイカーをRubyで作ってみた

自称インド人のナンディさんがやっていた「あのミニゲーム」を、
開発も女神転生も未経験の素人が覚えたての知識で作る、それだけの内容。

##コードブレイカーとは
Hit&Blowとも呼ぶらしい、数当てゲーム。
各桁の数字が重複しない、何桁かの数字をディーラーが作り、
プレイヤーはその数字が何であるかを推理して答えるゲームである。
答えた数字が桁まで一致していれば「ヒット」、
数字はあっているが桁が違うなら「ブロー」となる。
全ての桁が一致すれば勝利となる。

少し前にフジテレビでやっていた「Numer0n」と基本ルールは同じ。
違いはアイテムがないことと、プレイヤーとディーラーに分かれていることぐらいか。

##必要なもの
プログラムを作る前に、必要なパーツを洗い出してみる。

[・同じ数字を含まないX桁の整数を生成する乱数](#同じ数字を含まないX桁の整数を生成する乱数)
[・不正な入力に対してエラーを返す処理](#不正な入力に対してエラーを返す処理)
[・ヒットとブローのカウント処理](#ヒットとブローのカウント処理)
[・予想が当たった時にそこでゲーム終了（勝利）とする処理](#予想が当たった時にそこでゲーム終了（勝利）とする処理)
[・プレイ回数以内に正解できなかった場合にゲーム終了とする処理](#プレイ回数以内に正解できなかった場合にゲーム終了とする処理)

このうち、
「不正な入力に対してエラーを返す処理」～「予想が当たった時にそこでゲーム終了（勝利）とする処理」は
メソッド用のファイル"codebreaker_method.rb"に記述し、
残りのパーツは本体のファイル"codebreaker.rb"に記述するものとする。

"codebreaker.rb"には最初にメソッド用のファイルを呼び出すためのメソッドを必ず書いておく。
同一ディレクトリから呼び出す前提なので、require_relativeで。
***
####同じ数字を含まないX桁の整数を生成する乱数
最初に、正解の数字aの元になる数字の配列codeを作る。
codeは完全な連番の配列なので、範囲オブジェクトから変換するのが手っ取り早い。

```Ruby:
code = (0..9).to_a    #=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

重複せずに取り出すにはsampleというお誂え向きなメソッドがあるので、それを使う。

```Ruby:
a = code.sample(3)
```
乱数なので呼び出す度に中身が変わるのではと思うかもしれないが、

```Ruby:
p a
```
のように変数名だけ出すのであれば中身は変わらない。
右辺のsampleメソッドさえ実行しなければいいだけ。

この正解の数字aを作る部分は一回のプレイにつき一度しか実行しないので、
本体"codebreaker.rb"の、require_relativeの次の所に書いておく。
***
####不正な入力に対してエラーを返す処理

次に、入力された数字をチェックして、不正であればエラーを返すメソッド"get_num"を
"codebreaker_method.rb"内に定義する。

get_numメソッドは、3桁の数字以外に対してはエラー文を表示して再入力を促す。
3桁の数字が入力されるまでは処理を繰り返す必要がある。

そこで、記述の簡略化も兼ねてloop文を使うことにする。
loop文は条件に関係なく処理を繰り返すが、loop内でbreakなどの処理中断のメソッドがあれば、
そこで繰り返しを抜けて次の処理に進むことが出来る。

また、if文だけで作ると、最初とif文内の各エラー処理にそれぞれ
「数値を入力させる処理」を書かないといけないが、
loopなら頭から繰り返すので最初に1回書くだけで済み、結果スリムなコードになる。

コメントも交えた設計は概ねこんな感じ。

```Ruby:
loop do
    #ユーザーの予想した数字を受け取り、配列に変換する

    #エラー処理
    #1.数字以外の文字がある時
    if ([条件式1])
    #2.重複する桁がある時
    elsif ([条件式2])
    #3.桁数が3桁でない時
    elsif ([条件式3])
    #問題なければbreakでループを抜ける
    else
        break
    end
end    
```
<br>
まず、入力された文字列を配列に変換する所から。
getsでコンソールから数字を入力させ、chompで末尾の改行を削除し、splitで配列にする。
splitは引数を"//"にすると、隙間なく連なった文字列でも分解して配列にしてくれる。

```Ruby:
2.6.3 :001 > b = gets.chomp.split(//)
123
 => ["1", "2", "3"] 
```

<br>

続いて、エラーの処理だが、想定されるエラーは、
　1.0-9の数字以外の文字が含まれている時
　2.桁数が3桁でない時
　3.同じ数字の桁がある時（122、333など）
...の三つ。
加えて、エラーメッセージを表示し、再入力を促す処理も必要。

1の数字以外の文字は、any?メソッドと正規表現を使う。
any?メソッドは、配列内に一つでも真である要素があればtrue、一つもなければfalseを返すメソッド。
これにブロックの代わりに正規表現を組み合わせてみた。

```Ruby:
if b.any?(/\D/)
    puts "数字ではない文字が含まれています。"
```

"\D"は正規表現で「0-9の数字以外の一文字」を表す。
<br>
2の桁数は、配列の長さを返すsizeメソッドで。

```Ruby:
elsif b.size != 3
    puts "3桁より多いか少ない数字です。"
```
<br>
3の数字の重複はsizeメソッドと、重複する要素を削除するuniqメソッドで対処。
1桁でも重複があると3桁未満になってしまうのでエラーになる、という仕組み。

この場合だと「12223」「12233」のように重複を削除すると3桁になる数字を取りこぼしそうだが、
2で既に3桁以外の数字を除外しているので問題ない。

```Ruby:
elsif b.uniq.size != 3
    puts "同じ数字の桁があります。"
```
<br>
以上の1～3のチェックで問題なければ、breakでループを抜け、
戻り値bを本体の"codebreaker.rb"に渡す。

```Ruby:
    else
        return b
        break
    end
end
```
<br>

以上がget_numメソッド。
全部まとめたものがこちら。

```Ruby:
def get_num
    loop do
        puts "数字が全て異なる3桁の整数を入力してください。"
        #ユーザーの予想した数字を受け取り、配列に変換する
        b = gets.chomp.split(//)
    
        #以下、エラー処理
        #1.数字以外の文字がある時
        if b.any?(/\D/)
            puts "数字ではない文字が含まれています。"
        #2.桁数が3桁でない時
        elsif b.size != 3
            puts "3桁より多いか少ない数字です。"
        #3.重複する桁がある時
        elsif b.uniq.size != 3
            puts "同じ数字の桁があります。"
        else
            return b
            break
        end
    end
end
```



***
####ヒットとブローのカウント処理

次は、ヒットとブローを返すメソッド"hit_and_blow"を定義する。

最初に説明したように、
答えた数字が桁（配列でいえばインデックス番号）まで一致していれば「ヒット」、
数字はあっているが桁が違うなら「ブロー」となる。

まず、ヒット。
「2つの配列を」「同じインデックス番号同士で」「each文を使って」チェックする方法を探していたら、
幸運にもちょうどほしかった物が見つかったので、リンクを貼っておく。
[【Rubyメモ】eachメソッドで複数の配列を同時にループさせる方法](https://freesworder.net/ruby-each-multiple/)

やり方は簡単に言うと、zipメソッドで配列aとbを合体させ、
同じインデックス番号の要素同士が結合した入れ子構造の配列を作って、
その中身を比較させる、というもの。

```Ruby:
2.6.3 :001 > a = [0,1,2]
 => [0, 1, 2] 
2.6.3 :002 > b = [0,1,2]
 => [0, 1, 2] 
2.6.3 :003 > p a.zip(b)
[[0, 0], [1, 1], [2, 2]]
 => [[0, 0], [1, 1], [2, 2]] 
```

後はこの小さな配列の中身をそれぞれxとyとし、
比較して値が等しければヒットの数を+1する。

この時気を付けてほしいのが、getsで入力させた数字は**自動的に文字列として取得される**という点。
最初にsampleメソッドで作ったランダム配列aの中身は**数値**なので、そのまま比較しても
「全部違う」という結果しか返ってこない。（ここで30分くらい悩みました）

なので、文字列である配列bの要素yにだけto_iメソッドを付けてやる。

```Ruby:
a.zip(b).each do |x, y|
  if x == y.to_i
    hit += 1
  end
end
```

尚、数値を入力させる所の"gets.chomp.split(//)"を"gets.chomp.to_i.split(//)"にすれば
数値オブジェクトの配列が作れるのでは？と思うかもしれないが、
splitメソッドが数値オブジェクトに対応していないのでエラーになる。
<br>
一方、ブローは割と簡単。
include?で配列bの要素が配列aにもあるかeachで見ていけばいいだけ。
ただ、to_iでbの要素を数値に変換するのを忘れないように。

```Ruby:
else
    b.each do |x|
        if a.include?(x.to_i) === true
            blow += 1
        end
    end
end
```

<br>
ここで思い出してほしい事が一つ。
**「ヒットとブローは重複しない」**
つまり、ヒットにカウントした桁はブローにカウントしてはいけない。逆も然り。
しかし、上述のコードには「ヒットにカウントした桁を除外する」部分はないので、
重複が発生し、ヒットとブローの合計数が3を超えてしまう。

では、どうするか。
プログラムの構造やルールから考えて、「ヒットの数の方がブローの数より優先される」ので、
ブローの処理の後に

```Ruby:
blow = blow - hit
```
とすることで調整。

最後に、戻り値は

```Ruby:
return "ヒット:#{hit} ブロー:#{blow}"
```
とすることで、本体での処理を簡略化。

ここまでをまとめたものが以下の通り。

```Ruby:
def hit_and_blow(a, b)
    #ヒットとブローのカウントを初期化
    hit, blow = 0, 0
    #ヒットのカウント
    a.zip(b).each do |x, y|
        if x == y.to_i
            hit += 1
        end
    end
    #ブローのカウント処理
    b.each do |x|
        if a.include?(x.to_i) === true
            blow += 1
        end
    end
#ブローのカウントからヒットを除外
blow = blow - hit
return "ヒット:#{hit} ブロー:#{blow}"
end
```
***
####予想が当たった時にそこでゲーム終了（勝利）とする処理
予想が当たる＝ヒット数が桁数に等しくなる、ということなので、
今回はヒット数が3の時にゲーム終了の処理を実行する。

ヒット数が3になった時点で残りプレイ回数に関係なく終了となるので、
「ループの最中で強制的に抜けさせる」ような処理が必要になる。

これに該当するのがexit。
exitを実行すると、それ以降の処理を（例外処理を除いて）無視して終了する。
この処理を、ヒットのカウントとブローのカウントの間に挿入する。

```Ruby:
def hit_and_blow(a, b)
    #ヒットとブローのカウントを初期化
    hit, blow = 0, 0
    #ヒットのカウント
    a.zip(b).each do |x, y|
        if x == y.to_i
            hit += 1
        end
    end
    #ヒットの数が3だった場合はここで終了（勝利）
    if hit == 3
        puts "ヒット:#{hit} ブロー:#{blow}"
        puts "おめでとう！正解！"
        exit
    #ヒットの数が3でない場合はブローのカウントへ
    else
        b.each do |x|
            if a.include?(x.to_i) === true
                blow += 1
            end
        end
    end
    #ブローのカウントからヒットを除外
    blow = blow - hit
    return "ヒット:#{hit} ブロー:#{blow}"
end
```

以上で、メソッドファイル"codebreaker_method.rb"の作成は終わり。
後は本体に「クリアできなかった場合の処理」を入れるだけ。
***
####プレイ回数以内に正解できなかった場合にゲーム終了とする処理

プレイ回数は正解の数字を作成する所の直後に書いておく。
正解した場合の処理を見るために、気持ち多めに設定すること。

```Ruby:
#正解のコードを乱数で決定する
code = (0..9).to_a
a = code.sample(3)
#残りプレイ回数を設定
credit = 10
```

プレイ回数が0になるまでのループ部分はwhile文で。

ループ部分の順序は以下の通り。

1.残りプレイ回数を表示
　↓
2.プレイヤーに予想の数字を入力させる（get_num実行）
　↓
3.予想の数字と正解の数字を比較し、結果を表示（hit_and_blow実行）⇒完全に一致なら即終了
　↓
4.残りプレイ回数を1減らす
　↓
（1.に戻る）

ヒットとブローの数はhit_and_blowメソッドの戻り値にメッセージごと設定してあるので、
puts hit_and_blow　で表示できる。

この部分をまとめたものがこちら。

```Ruby:
while credit > 0
    puts "残りプレイ回数:#{credit}"
    #3桁の数字の予想をプレイヤーに入力させる
    b = get_num

    #正解aと予想bを比較し、ヒットとブローをカウント
    puts hit_and_blow(a, b)
    credit -= 1
end
```
<br>
最後に、プレイ回数が0になったらループを抜けて、正解の数字を表示して終了。
数字といっても中身は配列なので、見た目だけでも数字に「整形」してやる必要がある。

配列の各要素を一つにつなげるにはjoinメソッドを使う。
引数を指定しなければ間に何も挟まないので、きれいな数字に見せられる。

```Ruby:
#残りプレイ回数が0の時は、正解の数字を表示して終了
puts "残念！正解は「#{a.join}」でした！"
```
<br>
実行結果はこんな感じ。まずはクリアした場合。

```Ruby:
student_45700:~/environment $ ruby codebreaker.rb
残りプレイ回数:10
数字が全て異なる3桁の整数を入力してください。
487
ヒット:1 ブロー:0
残りプレイ回数:9
数字が全て異なる3桁の整数を入力してください。
459
ヒット:1 ブロー:0
残りプレイ回数:8
数字が全て異なる3桁の整数を入力してください。
412
ヒット:1 ブロー:1
残りプレイ回数:7
数字が全て異なる3桁の整数を入力してください。
423
ヒット:1 ブロー:1
残りプレイ回数:6
数字が全て異なる3桁の整数を入力してください。
402
ヒット:1 ブロー:0
残りプレイ回数:5
数字が全て異なる3桁の整数を入力してください。
430
ヒット:2 ブロー:0
残りプレイ回数:4
数字が全て異なる3桁の整数を入力してください。
435
ヒット:2 ブロー:0
残りプレイ回数:3
数字が全て異なる3桁の整数を入力してください。
436
ヒット:2 ブロー:0
残りプレイ回数:2
数字が全て異なる3桁の整数を入力してください。
439
ヒット:2 ブロー:0
残りプレイ回数:1
数字が全て異なる3桁の整数を入力してください。
431
ヒット:3 ブロー:0
おめでとう！正解！
```

続いて、クリアできなかった場合。（creditを5に変更しています）

```Ruby:
student_45700:~/environment $ ruby codebreaker.rb
残りプレイ回数:5
数字が全て異なる3桁の整数を入力してください。
123
ヒット:0 ブロー:0
残りプレイ回数:4
数字が全て異なる3桁の整数を入力してください。
456
ヒット:1 ブロー:1
残りプレイ回数:3
数字が全て異なる3桁の整数を入力してください。
548
ヒット:1 ブロー:0
残りプレイ回数:2
数字が全て異なる3桁の整数を入力してください。
596
ヒット:2 ブロー:0
残りプレイ回数:1
数字が全て異なる3桁の整数を入力してください。
597
ヒット:1 ブロー:0
残念！正解は「506」でした！
```

最後は、エラーの場合。

```Ruby:
student_45700:~/environment $ ruby codebreaker.rb
残りプレイ回数:5
数字が全て異なる3桁の整数を入力してください。
12233
3桁より多いか少ない数字です。
数字が全て異なる3桁の整数を入力してください。
777
同じ数字の桁があります。
数字が全て異なる3桁の整数を入力してください。
abc
数字ではない文字が含まれています。
数字が全て異なる3桁の整数を入力してください。
158
ヒット:1 ブロー:1
残りプレイ回数:4
数字が全て異なる3桁の整数を入力してください。
```