RubyのArray#productでスロット

Feb 22, 2020 21:12 · 1231 words · 3 minute read

RubyのArray#productを知らなかったのでメモ。

Slackのカスタムレスポンスを使ってスロットみたいなことができる。

そのためにはカスタムレスポンスに出目をバーっと設定する必要がある。

例えばスロットの図柄が:neko:, :kuma:, :inu:(カスタム絵文字)の3種類なら、

:neko: :neko: :neko:
:neko: :neko: :kuma:
:neko: :neko: :inu:
:neko: :kuma: :neko:
:neko: :kuma: :kuma:
:neko: :kuma: :inu:
:neko: :inu: :neko:
:neko: :inu: :kuma:
:neko: :inu: :inu:
:kuma: :neko: :neko:
:kuma: :neko: :kuma:
:kuma: :neko: :inu:
:kuma: :kuma: :neko:
:kuma: :kuma: :kuma:
:kuma: :kuma: :inu:
:kuma: :inu: :neko:
:kuma: :inu: :kuma:
:kuma: :inu: :inu:
:inu: :neko: :neko:
:inu: :neko: :kuma:
:inu: :neko: :inu:
:inu: :kuma: :neko:
:inu: :kuma: :kuma:
:inu: :kuma: :inu:
:inu: :inu: :neko:
:inu: :inu: :kuma:
:inu: :inu: :inu:

こんな感じで設定する。
図柄が3つ揃った時は何らかの当たった演出を加えたりするといい感じになる。

図柄を変えたり増えたり減らしたり自由に設定したいので、そのためにはこれを簡単に吐き出せる必要がある。

スロットには3つのリールがあるので、図柄が\(N\)種類あるとき\(N ^ 3\)個の組み合わせを全列挙したい。

単純に考えればえいやっと3重ループで書ける。

members = ["neko", "kuma", "inu"]

comb = []

members.each do |left|
  members.each do |center|
    members.each do |right|
      comb << ":#{left}: :#{center}: :#{right}:"
    end
  end
end

puts comb

うーんいい感じ、以上です。

〜Fin〜

〜再開〜

もうちょい違う書き方できないかな〜ってことでArray#productて便利なもののがあるのを知った。 こう書ける。

members = ["neko", "kuma", "inu"]

comb = members.product(members, members).map do |left, center, right|
  ":#{left}: :#{center}: :#{right}:"
end

puts comb

3重ループがなくなった。便利。ただ元の3重ループと比べてわかりやすいかは謎。

今回こんな感じで列挙したものはデカルト積とか直積集合とか呼ぶらしい。
なるほどproduct

自分でも単純な実装を書いてみた。

def product(*arrays)
  ret = []

  arrays.each do |arr|
    memo = []

    if ret.empty?
      ret = arr
      next
    end

    ret.each do |r|
      arr.each do |e|
        memo << [*r, e]
      end
    end

    ret = memo
  end

  ret
end

なんかif ret.empty?で初回ループの特別処理してるところがあんまセンスない感じだけど一応同じようにデカルト積を求められる。

色々みて回ってこういう実装を見つけた:aaw/cartesian-product

これふつうに感動的で、
特にこの部分:https://github.com/aaw/cartesian-product/blob/master/lib/cartesian-product.rb#L21-L27

def index_to_item(index)
  @arrays.map do |array|
    element = array[index % array.length]
    index /= array.length
    element
  end.reverse
end

このインデックスの操作がテクい感じがする。

でもよくわからないから、

arr1 = [1, 2, 3]
arr2 = ['a', 'b', 'c']

prod = CartesianProduct.new(arr1, arr2)
prod.each { |element| p element }

こういう感じで要素数3の配列を2つ与えて動かしたときのarr1arr2のインデックスの遷移を追ってみると、

arr1: [0, 0, 0, 1, 1, 1, 2, 2, 2]
arr2: [0, 1, 2, 0, 1, 2, 0, 1, 2]

となって、3つ与えたときは、

arr1: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2]
arr2: [0, 0, 0, 1, 1, 1, 2, 2, 2, 0, 0, 0, 1, 1, 1, 2, 2, 2, 0, 0, 0, 1, 1, 1, 2, 2, 2]
arr3: [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]

こうなる。

余りをとったインデックスで周期的にアクセスしつつ、

index /= array.length

ここで最後の要素に来た時だけ次の配列のインデックスに1でアクセスするてことか〜。あたまいー。

配列(集合)の操作って業務じゃあんまり複雑なことやらないし知らないこと多いから都度書いていこうと思う。