nilbus/pinewood-derby

View on GitHub
app/models/heat.rb

Summary

Maintainability
A
1 hr
Test Coverage
class Heat < ActiveRecord::Base
  has_many :runs, dependent: :destroy
  has_many :contestants, through: :runs

  scope :current, -> { where(status: 'current') }
  scope :complete, -> { where(status: 'complete').order('sequence DESC, created_at DESC').includes(runs: :contestant) }
  scope :most_recent, -> { complete.limit(1) }
  scope :upcoming, -> { where(status: 'upcoming').order('sequence, created_at').includes(runs: :contestant) }
  scope :upcoming_incomplete, -> { where(status: 'upcoming').joins(:runs).group('heats.id').having('count(runs.id) < ?', DerbyConfig.lane_count) }

  validates :status,   presence: true, inclusion: {in: %w(upcoming current complete)}
  validates :sequence, presence: true

  class << self
    def fill_lineup(options = {})
      upcoming_incomplete.readonly(false).destroy_all
      races_to_queue = options.fetch(:races, 3)
      contestants_per_heat = DerbyConfig.lane_count
      races_to_queue.times do
        break if upcoming.count >= races_to_queue
        chosen_contestants = {}
        contestants_per_heat.times do |i|
          lane = i + 1
          next_contestant = Contestant.next_suitable(lane: lane, exclude: chosen_contestants.values)
          next unless next_contestant
          chosen_contestants[lane] = next_contestant
        end
        break if chosen_contestants.none?
        Heat.create_upcoming_from_contestants chosen_contestants
      end
    end

    def create_upcoming_from_contestants(contestants_by_lane)
      transaction do
        heat = Heat.create! sequence: next_sequence, status: 'upcoming'
        contestants_by_lane.each do |lane, contestant|
          heat.runs.create! contestant: contestant, lane: lane
        end

        heat
      end
    end

    def post_results(results)
      heat = current.first
      return unless heat
      heat.add_run_times(results)
      heat.complete!
    end

    def create_practice(options = {})
      Heat.transaction do
        raise Notice.new "There's already a race going" if Heat.current.any?
        contestants = options[:contestants] || Contestant.limit(DerbyConfig.lane_count)
        raise Notice.new "Add contestants first" if contestants.none?
        heat = create! sequence: -1, status: 'current'
        contestants.each_with_index do |contestant, i|
          lane = i + 1
          Run.create! contestant: contestant, heat: heat, lane: lane
        end

        heat
      end
    end

    def next_sequence
      Heat.order('sequence DESC').limit(1).pluck(:sequence).first || 1
    end
  end

  def start(options = {})
    update_attributes status: 'current' if Heat.current.count == 0
    raise "Can't start; there's already a heat running" unless current?
    options.fetch(:sensor_watch, SensorWatch).start_race

    true
  end

  def add_run_times(results)
    runs_by_lane = runs.group_by(&:lane)
    results.each do |result|
      run = runs_by_lane[result[:track].to_i].try :first
      run.set_time result[:time] if run
    end
  end

  def complete!
    update_attributes! status: 'complete'
  end

  def cancel!
    return false unless status == 'current'
    update_attributes! status: 'upcoming'
  end

  def current?
    status == 'current'
  end

  def upcoming?
    status == 'upcoming'
  end
end