16
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ElixirAdvent Calendar 2022

Day 4

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

Last updated at Posted at 2022-12-02

はじめに

この記事は、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 を返します。

リストに対象の値が含まれているかどうか調べる
  @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()の戻り値を返すだけですね。

リストに対象の値が含まれているかどうか調べ、最初に見つかったインデックス番号を返す
  @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
実行例
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?() という関数があります。

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

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

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

互いに異なる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
実行例
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]

答えの候補を生成する

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

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

[[[[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()を使って、フラットなリストに変換しています。
(ちなみに、リストへの新たな要素の追加は、末尾より先頭に追加した方が速いのですが、末尾の方が理解しやすいので、今回は末尾に追加しています)

解の候補を生成する
  @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
実行例
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, ...],
  [...],
  ...
]

1回目のチャレンジの際は、上記のリスト中の5040個の要素の中から、ランダムに1つを選んでチャレンジしますが、

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

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

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

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

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

入力した数値と答えの数値を比較し、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!

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
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
実行例
iex(16)> Lib_hit_and_blow.response_check(4, [0,1,2,3], [2,1,5,6])
{1, 1}

ちなみに、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で値の更新ができないという特徴に、なかなか慣れず、
移植には1週間ほどかかってしまいました。

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

16
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?