📚

ダブルスのシャッフル機能について考えてみた(Ruby)

2022/12/16に公開約4,300字

はじめに

バドミントンサークルで、自動で試合の組み合わせを決めてくれるアプリを欲しかったので、シャッフル機能を検討しました。
フロントはActionViewを想定しています。

Image from Gyazo

シャッフルの条件

  • 各人の試合出場回数をカウントできること
  • 完全に同じ試合が出た場合は再度シャッフル
  • 入力値はコート数、人数
  • 全てダブルスの試合

使用したRubyのメソッド紹介

shuffle

配列の中身をランダムシャッフルしてくれます。
!をつけると破壊的になります。
https://miyamae.github.io/rubydoc-ja/2.7/#!/class/-array.html?I_SHUFFLE

sample

配列からランダムに一つ値を返します。引数を指定すると複数返すことも可能です。
https://miyamae.github.io/rubydoc-ja/2.7/#!/class/-array.html?I_SAMPLE

combination

配列から引数で使用したサイズの組み合わせを返します。
10個の配列で引数2を指定した場合、10C2(=45)通りの配列を返してくれます。
https://miyamae.github.io/rubydoc-ja/2.7/#!/class/-array.html?I_COMBINATION

テーブル

シャッフル機能とは関係無いテーブルやカラムは除外しています。

  • Events
    練習をイベントとして登録する
  • Attendances
    イベント参加者を登録する
  • Matches
    試合の組み合わせを登録する
    Image from Gyazo

実装

match.rb
class Match < ApplicationRecord
  belongs_to :event

  # unfixed: 調整中の組み合わせ, fixed: 決定した組み合わせ, past: 終了した組み合わせ
  enum state: { unfixed: 0, fixed: 1, past: 2 }
end
event.rb
class Event < ApplicationRecord
  has_many :attendances, dependent: :destroy
  has_many :matches, dependent: :destroy

  # 仮決定の組み合わせ
  def match_unfixed_array
    match_set = []
    matches.unfixed.each do |m|
      match_set << [m.user_a, m.user_b, m.user_c, m.user_d]
    end
    match_set
  end

   # 決定した組み合わせ
  def match_fixed_array
    match_set = []
    matches.fixed.each do |m|
      match_set << [m.user_a, m.user_b, m.user_c, m.user_d]
    end
    matches.past.each do |m|
      match_set << [m.user_a, m.user_b, m.user_c, m.user_d]
    end
    match_set
  end

  # 仮組
  def match_array
    match_set = []
    matches.each do |m|
      match_set << [m.user_a, m.user_b, m.user_c, m.user_d]
    end
    match_set
  end

  # 引数はmatchオブジェクトが入り、過去の試合と重複していないかを確認
  def check_duplication_match(match)
    match_check = [match[0], match[1], match[2], match[3]]
    !match_array.include?(match_check)
  end

  # 引数はmatchオブジェクトが入り、現在の試合の中でユーザーが重複していないか確認
  def check_duplication_member(match)
    match_check = match_unfixed_array.flatten
    match.each do |m|
      match_check -= [m]
    end
    match_check.count == match_unfixed_array.flatten.count
  end

  # viewsで使用 ユーザーの試合回数を算出
  def match_count_player(user)
    match_fixed_array.flatten.count(user)
  end

  def matches_make
    member = attendances.absent.pluck(:user_id)
    pairs = member.combination(2).to_a
    round_robin = []
    pairs.combination(2).to_a.each do |i|
      if i.flatten.uniq.count == 4
        round_robin << [i[0][0], i[0][1], i[1][0], i[1][1]]
      end
    end
    round_robin
  end
end
matches_controller.rb
# 1試合目のみ実行される
def create
  event = Event.find(params[:event_id])
  event.matches.unfixed.destroy_all
  court_num = params[:match][:court_num].to_i
  matches = event.matches_make.shuffle
  match = matches.sample
  if event.matches.empty?
    event.matches.create(state: 'unfixed', user_a: match[0], user_b: match[1], user_c: match[2], user_d: match[3])
  end
  matches.each do |m|
    # court_numはコート数で、試合の組み合わせがコート数と同じだけ決まった段階で繰り返しを終了させる
    if court_num == event.matches.unfixed.count
      break
    end
    # メンバーの重複および過去の試合と同じ組み合わせになっていないかを確認
    if event.check_duplication_match(m) && event.check_duplication_member(m)
      event.matches.create(state: 'unfixed', user_a: m[0], user_b: m[1], user_c: m[2], user_d: m[3])
    end
  end
end

# 既に1試合以上fixed終えている場合はこちらのメソッドを
def match_fixed
  event = Event.find(params[:event_id])
  # 現在確定している試合を終了させステータスに変更する
  event.matches.fixed.update(state: 'past')
  # 調整中の組み合わせを全て削除する
  event.matches.unfixed.destroy_all
  # 新たな組み合わせを作成
  params[:match][:matches].each do |i|
    user_a = event.attendances.find(i['user_a']).user.id
    user_b = event.attendances.find(i['user_b']).user.id
    user_c = event.attendances.find(i['user_c']).user.id
    user_d = event.attendances.find(i['user_d']).user.id
    event.matches.create(state: 'fixed', user_a: user_a, user_b: user_b, user_c: user_c, user_d: user_d)
  end
end

最後に

スーパーFat Controllerなのでリファクタリングのしがいがありますね!!
これ以上モデルにメソッドを寄せてもFat Modelになっちゃうので、フォームオブジェクトで全部まとめる方がいいかも?

Discussion

ログインするとコメントできます