Class: Pairer::Board

Inherits:
ApplicationRecord show all
Defined in:
app/models/pairer/board.rb

Constant Summary collapse

RECENT_RESHUFFLE_DURATION =
1.minute.freeze
NUM_CANDIDATE_GROUPINGS =
5

Instance Method Summary collapse

Methods inherited from ApplicationRecord

#in_order_of, #to_param

Instance Method Details

#current_groupsObject



23
24
25
# File 'app/models/pairer/board.rb', line 23

def current_groups
  groups.where(board_iteration_number: current_iteration_number)
end

#roles=(val) ⇒ Object



33
34
35
36
37
38
39
# File 'app/models/pairer/board.rb', line 33

def roles=(val)
  if val.is_a?(Array)
    self[:roles] = val.map{|x| x.presence&.strip }.uniq(&:downcase).compact.sort.join(";;")
  else
    raise "invalid behaviour"
  end
end

#roles_arrayObject



41
42
43
# File 'app/models/pairer/board.rb', line 41

def roles_array
  self[:roles]&.split(";;") || []
end

#shuffle!Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'app/models/pairer/board.rb', line 45

def shuffle!
  new_groups = []

  prev_iteration_number = current_iteration_number
  next_iteration_number = current_iteration_number + 1

  available_person_ids = people.select{|x| !x.locked? }.collect(&:public_id)
  available_roles = roles_array

  ### Build New Groups
  groups.where(board_iteration_number: prev_iteration_number).each do |g|
    if g.locked?
      ### Clone Locked Groups

      new_group = g.dup

      new_group.assign_attributes(
        public_id: nil,
        board_iteration_number: next_iteration_number,
      )

      new_groups << new_group

      available_person_ids = (available_person_ids - g.person_ids_array)

      available_roles = (available_roles - new_group.roles_array)
    else
      ### Retain Position of Locked People within Existing Groups

      group_locked_person_ids = (g.person_ids_array - available_person_ids)

      if group_locked_person_ids.any?
        new_group = groups.new(
          board_iteration_number: next_iteration_number,
          roles: [],
          person_ids: group_locked_person_ids,
        )

        new_groups << new_group

        available_person_ids = (available_person_ids - group_locked_person_ids)
      end
    end
  end

  self.increment!(:current_iteration_number)

  if available_person_ids.any?
    pair_stats_hash = stats_hash_for_two_pairs

    ### Assign People to Non-Full Unlocked Groups
    new_groups.select{|x| !x.locked? }.each do |g|
      break if available_person_ids.empty?

      num_to_add = self.group_size - g.person_ids_array.size

      next if num_to_add <= 0

      if available_person_ids.size < num_to_add
        ### Add to group whatever is left

        g.person_ids = g.person_ids_array + available_person_ids

        available_person_ids = []

        break
      end

      group_size_combinations = available_person_ids.combination(num_to_add).map{|x| x + g.person_ids_array }.shuffle

      ### Choose group using minimum score
      chosen_person_ids = group_size_combinations.min_by do |person_ids|
        person_ids.combination(2).map(&:sort).sum{|k| pair_stats_hash[k] || 0 }
      end

      g.person_ids = (g.person_ids_array | chosen_person_ids).sort

      available_person_ids = (available_person_ids - chosen_person_ids)
    end

    ### Assign People to New Groups
    while available_person_ids.any? do
      if available_person_ids.size <= self.group_size
        ### Create group using whats left

        new_groups << groups.new(
          board_iteration_number: next_iteration_number,
          person_ids: available_person_ids,
        )

        available_person_ids = []
      else
        group_size_combinations = available_person_ids.combination(self.group_size).to_a.shuffle

        ### Choose group using minimum score
        chosen_person_ids = group_size_combinations.min_by do |person_ids|
          person_ids.combination(2).map(&:sort).sum{|k| pair_stats_hash[k] || 0 }
        end

        new_groups << groups.new(
          board_iteration_number: next_iteration_number,
          person_ids: chosen_person_ids,
        )

        available_person_ids = (available_person_ids - chosen_person_ids)
      end
    end

    ### Shuffle Roles
    available_roles = available_roles.shuffle

    unlocked_new_groups = new_groups.select{|x| !x.locked? }

    ### Assign Roles to Groups
    available_roles.in_groups(unlocked_new_groups.size, false).each_with_index do |roles, i|
      unlocked_new_groups[i].roles = unlocked_new_groups[i].roles_array + roles
      available_roles = available_roles - roles
    end

    ### Save New Groups
    new_groups.each{|x| x.save! }
  end

  ### Delete empty groups
  groups
    .where(person_ids: [nil, ""])
    .each{|x| x.destroy! }

  ### Delete outdated groups
  groups
    .where.not(id: tracked_groups.collect(&:id))
    .each{|x| x.destroy! }

  ### Ensure stats do not contain bogus entries caused by re-shuffling, groups created less than x time ago are deleted upon shuffle
  if !Rails.env.test? || (Rails.env.test? && ENV['DELETE_RECENT_RESHUFFLED'].to_s == "true")
    groups
      .where.not(id: new_groups.collect(&:id))
      .where("#{Pairer::Group.table_name}.created_at >= ?", RECENT_RESHUFFLE_DURATION.ago)
      .each{|x| x.destroy! }
  end

  ### Reload groups to fix any issues with caching after creations and deletions
  groups.reload

  return true
end

#statsObject



192
193
194
195
196
197
198
199
200
# File 'app/models/pairer/board.rb', line 192

def stats
  array = []

  stats_hash_for_two_pairs.sort_by{|k,count| -count }.each do |person_ids, count|
    array << [person_ids, count]
  end

  return array
end

#tracked_groupsObject



27
28
29
30
31
# File 'app/models/pairer/board.rb', line 27

def tracked_groups
  groups
    .order(board_iteration_number: :desc)
    .where("board_iteration_number > #{current_iteration_number - num_iterations_to_track}")
end