Elixirでつくるn桁版Hit&Blow(ヌメロン(Numer0n))の出題者側プログラムおよび質問者側プログラム

## はじめに

この記事は、Pythonで作成した、

**n桁版Hit&Blow(ヌメロン(Numer0n))の出題者側プログラムおよび質問者側プログラム**
https://qiita.com/gx3n-inue/items/396eddf69fbfa1eacc65

を、Elixirに移植した話の紹介です。

日本国内では **「Hit&Blow (ヒット・アンド・ブロー)」** という名前で紹介されることが多い数当て遊びの一種ですが、海外では **Bulls&Cows** などとも呼ばれています。
派生形のゲームとしては、テレビ番組でも取り上げられた **Numer0n(ヌメロン)** が有名ですね。

今回、Elixirで作成したソースコードは下記に配置しております。
https://github.com/NobuyukiInoue/exHitAndBlow

**桁数n** は、 **2桁〜10桁** の中から選ぶことができます。

### offenceモード（質問者モード）
人間側が用意した番号を、コンピュータに当てさせるモードです。

コンピュータ側がランダムで候補を選択してきますので、表示された番号と自分の答えを照合して、
Hit数とBlow数を答えてあげてください。
（最小化戦略等はまだ実装していません。4桁の場合で、平均回答数は5.4回程度です。）

```
$ ./main offence 4
N ... 4

(remaining count = 5040) Is your number 0329 ?
[1] : please input H, B = 1,2
input response is Hit = 1, Blow = 2
（以下、Hit = 4 まで繰り返し）
```

回答が面倒な時は、事前に答えを与えておいて自動で回答する、自動回答モードもあります。

```
$ ./main offence 4 false 1234
N ... 4
set answer number ... 1234

(remaining count = 5040) Is your number 7986 ?
input response is Hit = 0, Blow = 0

(remaining count =  360) Is your number 5142 ?
input response is Hit = 0, Blow = 3

(remaining count =   88) Is your number 0521 ?
input response is Hit = 0, Blow = 2

(remaining count =   16) Is your number 2453 ?
input response is Hit = 0, Blow = 3

(remaining count =    3) Is your number 1234 ?
input response is Hit = 4, Blow = 0
calculate successful.

===== challenge history ======
[1] (5040) --> 7986 (0, 0)
[2] ( 360) --> 5142 (0, 3)
[3] (  88) --> 0521 (0, 2)
[4] (  16) --> 2453 (0, 3)
[5] (   3) --> 1234 (4, 0)
```

### defenceモード（出題者モード）
コンピュータ側が用意した番号を、人間側が推測して当てていくモードです。
（自分で考えるのは面倒なので、もう一つウインドウを開いて、offenceモードが指定してきた番号を入力してあげても良いかも）

```
$ ./main defence 4
N ... 4
[0] : select number xxxx = 0123
set select number ... 0123
input response is Hit = 0, Blow = 2
[1] : select number xxxx = 4567
set select number ... 4567
input response is Hit = 1, Blow = 0
[2] : select number xxxx =
（以下、Hit = 4 まで繰り返し）
```

### repeatモード
offenceモード（質問者モード）を、指定回数自動で繰り返すモードです。

ちなみに、5040問チャレンジさせても、
(M1 Macbook Air 2020)では、13秒台で回答が終わります。
Python3版では、36秒台でしたので、Elixir、かなり速いですねー

全ての回答の最後には、何回で答えまでだどりついたかの集計も表示されます。

```
ResultCount[5036] = 5
ResultCount[5037] = 6
ResultCount[5038] = 5
ResultCount[5039] = 4
======== distribution ========
1 ... 2
2 ... 9
3 ... 92
4 ... 632
5 ... 1820
6 ... 1821
7 ... 614
8 ... 47
9 ... 3
Distribution list Total = 5040
==============================
Total Questions = 27551
Total Average   = 5.466468253968254
==============================
Total execution time ... 13.828[s]
```

## 処理内容

Hit&Blowで必須となるいくつかの基本的な処理だけ、Elixirでの作成例を紹介させていただきます。

基本的には、PythonではWhileやForなどで記述していた繰り返し処理は、
再帰処理で記述していますが、Enum.reduce()などを駆使した書き方もあるかもしれません

### リストの中に指定した要素が含まれているかどうか調べる

まずは、、リストの中に、指定した要素が含まれているかを調べる処理です。
重複なしのn桁の番号を生成するには、チェック時に必須です。

**Enum.find_index()** で実現できますが、**fn** 以下の処理まで記述していると見づらずくなるので、
下記のような自作の関数から呼び出すようにしています。

リストarrの中から、targetの値が見つかったら **true**、見つからなかった場合は **false** を返します。

```Elixir:リストに対象の値が含まれているかどうか調べる
  @spec enum_contains(arr :: [integer], target :: integer) :: bool
  def enum_contains(arr, target) do
    not is_nil(Enum.find_index(arr, fn item -> item == target end))
  end
```
今回のHit&Blowでは見つかった位置のインデックス番号は必要ありませんが、
見つかったインデックス番号を返すのであれば、Enum.find_index()の戻り値を返すだけですね。

```Elixir:リストに対象の値が含まれているかどうか調べ、最初に見つかったインデックス番号を返す
  @spec enum_index(arr :: [integer], target :: integer) :: integer | nil
  def enum_index(arr, target) do
    Enum.find_index(arr, fn item -> item == target end)
  end
```

```Elixir:実行例
iex(10)> Lib_hit_and_blow.enum_contains([0,1,2,3,4,5],4)
true
iex(11)> Lib_hit_and_blow.enum_contains([0,1,2,3,4,5],9)
false
iex(11)> Lib_hit_and_blow.enum_index([0,1,2,3,4,5],5)
5
iex(12)> Lib_hit_and_blow.enum_index([0,1,2,3,4,5],6)
nil
```

ちなみに、文字列の中に、指定した文字列が含まれているか調べるには、
***String.contains?()*** という関数があります。

```Elixir
iex(12)> target="abcdefg"
"abcdefg"
iex(13)> String.contains?(target, "cd")
true
iex(14)> String.contains?(target, "gh")
false
```

### 互いに異なるn桁のランダムな番号を返す

答えの番号や、チャレンジする番号は乱数を使って生成しています。
乱数を使ってn桁の（各桁の値が重複しない）ランダムな番号を生成する処理を、以下のように記述してみました。

```Elixir:互いに異なるn桁のランダムな番号を返す(random.randint()版)
  @spec create_random_n_digits_number(n :: integer) :: [integer]
  def create_random_n_digits_number(n) do
#   :rand.seed :os.timestamp
    create_random_n_digits_number(n, [])
  end

  @spec create_random_n_digits_number(n :: integer, nums :: [integer]) :: [integer]
  def create_random_n_digits_number(n, nums) do
    if Enum.count(nums) == n do
      nums
    else
      num = (:rand.uniform 10) - 1
      if enum_contains(nums, num) do
        create_random_n_digits_number(n, nums)
      else
        create_random_n_digits_number(n, [num] ++ nums)
      end
    end
  end
```

```Elixir:実行例
iex(3)> Lib_hit_and_blow.create_random_n_digits_number(4)
[4, 7, 9, 1]
iex(4)> Lib_hit_and_blow.create_random_n_digits_number(10)
[7, 9, 5, 4, 0, 6, 1, 8, 3, 2]
```

### 答えの候補を生成する

１回目の回答の候補は、４桁の場合は(10x9x8x7=)5040通りありますが、
5040通りの候補を[integer]型で生成する処理は、以下のように作成してみました。

1桁目、２桁目...というように、再帰で生成しているので、そのままだと

***[[[[0,1,2,3],[0,1,2,4],[0,1,2,5], ...[0,1,2,9]],[[1,0,2,3],[1,0,2,4], ...]]]***

といったネストしたリストとなってしまうので、
各再帰処理から戻った直後に、Enum.flat_map(）を使って、フラットなリストに変換しています。
（ちなみに、リストへの新たな要素の追加は、末尾より先頭に追加した方が速いのですが、末尾の方が理解しやすいので、今回は末尾に追加しています）

```Elixir:解の候補を生成する
  @spec create_target_numbers(n :: integer) :: [[integer]]
  def create_target_numbers(n) do
    if n > 0 do
      target_numbers =
      for i <- 0..9 do
        create_target_numbers(n - 1, [i])
      end
      Enum.flat_map(target_numbers, fn array -> Enum.filter(array,fn item -> item != 0 end ) end)
    end
  end

  @spec create_target_numbers(n :: integer, work_number :: [integer]) :: [integer]
  def create_target_numbers(0, work_number) do
    work_number
  end

  def create_target_numbers(n, work_number) do
    target_numbers =
    for i <- 0..9, not enum_contains(work_number, i) do
      create_target_numbers(n - 1, work_number ++ [i])
    end
    if n > 1 do
      Enum.flat_map(target_numbers, fn array -> Enum.filter(array,fn item -> item != 0 end ) end)
    else
      target_numbers
    end
  end
```

```Elixir:実行例
iex(13)> Lib_hit_and_blow.create_target_numbers(4)
[
  [0, 1, 2, 3],
  [0, 1, 2, 4],
  [0, 1, 2, 5],
(中略)
  [0, 1, 8, 5],
  [0, 1, 8, ...],
  [0, 1, ...],
  [0, ...],
  [...],
  ...
]
```
１回目のチャレンジの際は、上記のリスト中の5040個の要素の中から、ランダムに１つを選んでチャレンジしますが、

２回目以降は、１回目の結果（Hit数, Blow数)から、条件に合わない番号は除外し、条件として可能性のある番号のみを、次の回答の候補群として再生成しています。
候補群の生成が終わると、その中からランダムに１つを選んで、次のチャレンジを行います。

あとは、、Hit数 == n になるまで繰り返すだけですね。

### 答えの番号とチャレンジした番号を照合し、Hit数とBlow数を返す

答えの数値が**0123**
チャレンジした数値が**2156**
の場合、 **Hit数は1** , **Blow数は1** となりますが、

２つの数値を比較して、Hit数とBlow数を返す処理は以下のように記述してみました。
**answer_number** が答えの数値、 **target_number** が候補として入力した数値です。
タブルで、**{Hit数, Blow数}** を返します。

```Elixir:入力した数値と答えの数値を比較し、Hit数とBlow数を返す（再帰のみで記述した初版）
  @spec response_check(n :: integer, answer_number :: [integer], target_number :: [integer]) :: {integer, integer}
  def response_check(n, answer_number, target_number) do
    response_check(n, answer_number, target_number, 0, 0, 0)
  end

  @spec response_check(n :: integer, answer_number :: [integer], target_number :: [integer], col :: integer, hit :: integer, blow :: integer) :: {integer, integer}
  def response_check(n, answer_number, target_number, col, hit, blow) do
    if col >= n do
      {hit, blow}
    else
      if Enum.at(target_number, col) == Enum.at(answer_number, col) do
        response_check(n, answer_number, target_number, col + 1, hit + 1, blow)
      else
        if enum_contains(answer_number, Enum.at(target_number, col)) do
          response_check(n, answer_number, target_number, col + 1, hit, blow + 1)
        else
          response_check(n, answer_number, target_number, col + 1, hit, blow)
        end
      end
    end
  end
```

その後、 @mnishiguchi にご助言をいただいて、（コメントに記載あり）
Enum.zip(), Enum.reduce() を使った書き換え例を紹介していただきました。
ありがとうございます。

> Elixirでもこういう書き方ができますよ。よかったら試してみてください。Happy coding!

```Elixir:
def response_check(n, answer_number, target_number) do
  for {n, m} <- Enum.zip(answer_number, target_number), reduce: {0, 0} do
    {hit, blow} ->
      cond do
        n == m -> {hit + 1, blow}
        n in target_number -> {hit, blow + 1}
        true -> {hit, blow}
      end
  end
end
```

```Elixir:
def response_check(n, answer_number, target_number) do
  Enum.zip(answer_number, target_number)
  |> Enum.reduce({0, 0}, fn
    {n, m}, {hit, blow} when n == m ->
      {hit + 1, blow}

    {n, _}, {hit, blow} ->
      if n in target_number,
        do: {hit, blow + 1},
        else: {hit, blow}
  end)
end
```

```Elixir:実行例
iex(16)> Lib_hit_and_blow.response_check(4, [0,1,2,3], [2,1,5,6])
{1, 1}
```

ちなみに、Pythonだと下記のような処理になります。

```Python:入力した数値と答えの数値を比較し、Hit数とBlow数を返す
def response_check(n:int, answer_number:str, target_number:str) -> (int, int):
    """
    H, B = 0, 0
    for i in range(0, n):
        if target_number[i] == answer_number[i]:
            H += 1
        else:
            for j in range(0, n):
                if i != j and target_number[i] == answer_number[j]:
                    B += 1
    return H, B
    """
    H, B = 0, 0
    for n, m in zip(answer_number, target_number):
        if n == m:
            H += 1
        elif n in target_number:
            B += 1
    return H, B
```

## 最後に

関数型言語では、各繰り返し処理の中はImutableで値の更新ができないという特徴に、なかなか慣れず、
移植には１週間ほどかかってしまいました。

再帰処理に頼ってループ処理を行なっていますが、Enum系の関数も使いこなさねば...