class RRSchedule::Schedule

Attributes

balanced_gt[RW]
balanced_ps[RW]
cycles[RW]
exclude_dates[RW]
flights[R]
gamedays[R]
group_flights[RW]
rounds[R]
rules[RW]
shuffle[RW]
start_date[RW]
teams[RW]

Public Class Methods

new(params={}) click to toggle source
# File lib/rrschedule.rb, line 9
def initialize(params={})
  @gamedays = []
  self.teams = params[:teams] || []
  self.cycles = params[:cycles] || 1
  self.shuffle = params[:shuffle].nil? ? true : params[:shuffle]
  self.balanced_gt = params[:balanced_gt].nil? ? true : params[:balanced_gt]
  self.balanced_ps = params[:balanced_ps].nil? ? true : params[:balanced_ps]      
  self.exclude_dates = params[:exclude_dates] || []
  self.start_date = params[:start_date] || Date.today
  self.group_flights = params[:group_flights].nil? ? true : params[:group_flights]
  self.rules = params[:rules] || []
  self
end

Public Instance Methods

generate(params={}) click to toggle source

This will generate the schedule based on the various parameters

# File lib/rrschedule.rb, line 24
def generate(params={})
  raise "You need to specify at least 1 team" if @teams.nil? || @teams.empty?
  raise "You need to specify at least 1 rule" if @rules.nil? || @rules.empty?

  arrange_flights
  init_stats

  @gamedays = []; @rounds = []


  @flights.each_with_index do |teams,flight_id|
    current_cycle = current_round = 0
    teams = teams.sort_by{rand} if @shuffle

    #loop to generate the whole round-robin(s) for the current flight
    begin
      t = teams.clone
      games = []

      #process one round
      while !t.empty? do
        team_a = t.shift
        team_b = t.reverse!.shift
        t.reverse!

        x = (current_cycle % 2) == 0 ? [team_a,team_b] : [team_b,team_a]

        matchup = {:team_a => x[0], :team_b => x[1]}
        games << matchup
      end
      #done processing round

      current_round += 1

      #Team rotation (the first team is fixed)
      teams = teams.insert(1,teams.delete_at(teams.size-1))

      #add the round in memory
      @rounds ||= []
      @rounds[flight_id] ||= []
      @rounds[flight_id] << Round.new(
        :round => current_round,
        :cycle => current_cycle + 1,
        :round_with_cycle => current_cycle * (teams.size-1) + current_round,
        :flight => flight_id,
        :games => games.collect { |g|
          Game.new(
            :team_a => g[:team_a],
            :team_b => g[:team_b]
          )
        }
      )
      #done adding round

      #have we completed a full round-robin for the current flight?
      if current_round == teams.size-1
        current_cycle += 1
        current_round = 0 if current_cycle < self.cycles
      end

    end until current_round == teams.size-1 && current_cycle==self.cycles
  end

  dispatch_games(@rounds)
  self
end
round_robin?(flight_id=0) click to toggle source

returns true if the generated schedule is a valid round-robin (for testing purpose)

# File lib/rrschedule.rb, line 116
def round_robin?(flight_id=0)
  #each round-robin round should contains n-1 games where n is the nbr of teams (:dummy included if odd)
  return false if self.rounds[flight_id].size != (@flights[flight_id].size*self.cycles)-self.cycles

  #check if each team plays the same number of games against each other
  @flights[flight_id].each do |t1|
    @flights[flight_id].reject{|t| t == t1}.each do |t2|
      return false unless face_to_face(t1,t2).size == self.cycles || [t1,t2].include?(:dummy)
    end
  end
  return true
end
to_s() click to toggle source

human readable schedule

# File lib/rrschedule.rb, line 101
def to_s
  res = ""
  res << "#{self.gamedays.size.to_s} gamedays\n"
  self.gamedays.each do |gd|
    res << gd.date.strftime("%Y-%m-%d") + "\n"
    res << "==========\n"
    gd.games.sort{|g1,g2| g1.gt == g2.gt ? g1.ps <=> g2.ps : g1.gt <=> g2.gt}.each do |g|
      res << "#{g.ta.to_s} VS #{g.tb.to_s} on playing surface #{g.ps} at #{g.gt.strftime("%I:%M %p")}\n"
    end
    res << "\n"
  end
  res
end
total_nbr_games() click to toggle source
# File lib/rrschedule.rb, line 91
def total_nbr_games
  total=0

  @flights.each do |teams|
     total += (teams.size / 2) * (teams.size-1)
  end
  total
end

Private Instance Methods

all_gt() click to toggle source

returns an array of all available game times / playing surfaces, all rules included.

# File lib/rrschedule.rb, line 336
def all_gt; @rules.collect{|r| r.gt}.flatten.uniq; end
all_ps() click to toggle source
# File lib/rrschedule.rb, line 337
def all_ps; @rules.collect{|r| r.ps}.flatten.uniq; end
arrange_flights() click to toggle source
# File lib/rrschedule.rb, line 131
def arrange_flights
  #a flight is a division where teams play round-robin against each other
  @flights = Marshal.load(Marshal.dump(@teams)) #deep clone

  #If teams aren't in flights, we create a single flight and put all teams in it
  @flights = [@flights] unless @flights.first.respond_to?(:to_ary)

  @flights.each_with_index do |flight,i|
    raise ":dummy is a reserved team name. Please use something else" if flight.member?(:dummy)
    raise "at least 2 teams are required" if flight.size < 2
    raise "teams have to be unique" if flight.uniq.size < flight.size
    @flights[i] << :dummy if flight.size.odd?
  end
end
dispatch_game(game) click to toggle source
# File lib/rrschedule.rb, line 207
def dispatch_game(game)
  if @cur_rule.nil?
    @cur_rule = @rules.select{|r| r.wday >= self.start_date.wday}.first || @rules.first
    @cur_rule_index = @rules.index(@cur_rule)
    reset_resource_availability
  end

  @cur_gt = get_best_gt(game)
  @cur_ps = get_best_ps(game,@cur_gt)

  @cur_date ||= next_game_date(self.start_date,@cur_rule.wday)
  @schedule ||= []

  #if one of the team has already plays at this gamedate, we change rule
  if @schedule.size>0
    games_this_date = @schedule.select{|v| v[:gamedate] == @cur_date}
    if games_this_date.select{|g| [game.team_a,game.team_b].include?(g[:team_a]) || [game.team_a,game.team_b].include?(g[:team_b])}.size >0
      @cur_rule_index = (@cur_rule_index < @rules.size-1) ? @cur_rule_index+1 : 0
      @cur_rule = @rules[@cur_rule_index]
      reset_resource_availability
      @cur_gt = get_best_gt(game)
      @cur_ps = get_best_ps(game,@cur_gt)
      @cur_date = next_game_date(@cur_date+=1,@cur_rule.wday)
    end
  end

  #We found our playing surface and game time, add the game in the schedule.
  @schedule << {:team_a => game.team_a, :team_b => game.team_b, :gamedate => @cur_date, :ps => @cur_ps, :gt => @cur_gt}
  update_team_stats(game,@cur_gt,@cur_ps)
  update_resource_availability(@cur_gt,@cur_ps)


  #If no resources left, change rule
  x = @gt_ps_avail.reject{|k,v| v.empty?}
  if x.empty?
    if @cur_rule_index < @rules.size-1
      last_rule=@cur_rule
      @cur_rule_index += 1
      @cur_rule = @rules[@cur_rule_index]
      #Go to the next date (except if the new rule is for the same weekday)
      @cur_date = next_game_date(@cur_date+=1,@cur_rule.wday) if last_rule.wday != @cur_rule.wday
    else
      @cur_rule_index = 0
      @cur_rule = @rules[@cur_rule_index]
      @cur_date = next_game_date(@cur_date+=1,@cur_rule.wday)
    end
    reset_resource_availability
  end
end
dispatch_games(rounds) click to toggle source

Dispatch games according to available playing surfaces and game times

# File lib/rrschedule.rb, line 147
def dispatch_games(rounds)

  rounds_copy =  Marshal.load(Marshal.dump(rounds)) #deep clone

  flat_games = []
  if group_flights
    while rounds_copy.flatten.size > 0 do
      @flights.each_with_index do |f,flight_index|
        r = rounds_copy[flight_index].shift
        flat_games << r.games if r
      end
    end          
  else
    flight_index = round_index = 0
    game_ctr = 0 
    while game_ctr < total_nbr_games
      if rounds_copy[flight_index][round_index] != nil
        game = rounds_copy[flight_index][round_index].games.shift
        if game
          flat_games << game 
          game_ctr += 1
        end
      end

      #check if round is empty
      round_empty=true
      @flights.size.times do |i| 
        round_empty = round_empty && (rounds_copy[i][round_index].nil? || rounds_copy[i][round_index].games.empty?)
      end
      
      if flight_index == @flights.size-1
        flight_index = 0
        round_index+=1 if round_empty
      else
        flight_index += 1
      end          
    end
  end
  
  flat_games.flatten!
  flat_games.each do |game|
    dispatch_game(game) unless [game.team_a,game.team_b].include?(:dummy)
  end

  #We group our schedule by gameday
  s=@schedule.group_by{|fs| fs[:gamedate]}.sort
  s.each do |gamedate,gms|
    games = []
    gms.each do |gm|
      games << Game.new(
        :team_a => gm[:team_a],
        :team_b => gm[:team_b],
        :playing_surface => gm[:ps],
        :game_time => gm [:gt]
      )
    end
    self.gamedays << Gameday.new(:date => gamedate, :games => games)
  end
end
face_to_face(team_a,team_b) click to toggle source

return matchups between two teams

# File lib/rrschedule.rb, line 316
def face_to_face(team_a,team_b)
  res=[]
  self.gamedays.each do |gd|
    res << gd.games.select {|g| (g.team_a == team_a && g.team_b == team_b) || (g.team_a == team_b && g.team_b == team_a)}
  end
  res.flatten
end
get_best_gt(game) click to toggle source
# File lib/rrschedule.rb, line 270
def get_best_gt(game)
  x = {}
  gt_left = @gt_ps_avail.reject{|k,v| v.empty?}
  
  if self.balanced_gt
    gt_left.each_key do |gt|
      x[gt] = [
        @stats[game.team_a][:gt][gt] + @stats[game.team_b][:gt][gt],
        rand(1000)
      ]
    end
    x.sort_by{|k,v| [v[0],v[1]]}.first[0]
  else
    gt_left.sort.first[0]
  end
end
get_best_ps(game,gt) click to toggle source
# File lib/rrschedule.rb, line 287
def get_best_ps(game,gt)
  x = {}
  
  if self.balanced_ps
    @gt_ps_avail[gt].each do |ps|
      x[ps] = [
        @stats[game.team_a][:ps][ps] + @stats[game.team_b][:ps][ps],
        rand(1000)
      ]
    end
    x.sort_by{|k,v| [v[0],v[1]]}.first[0]
  else
    @gt_ps_avail[gt].first[0]
  end
end
init_stats() click to toggle source

Count the number of times each team plays on a given playing surface and at what time. That way we can balance the available playing surfaces/game times among competitors.

# File lib/rrschedule.rb, line 326
def init_stats
  @stats = {}
  @teams.flatten.each do |t|
    @stats[t] = {:gt => {}, :ps => {}}
    all_gt.each { |gt| @stats[t][:gt][gt] = 0 }
    all_ps.each { |ps| @stats[t][:ps][ps] = 0 }
  end
end
next_game_date(dt,wday) click to toggle source

get the next gameday

# File lib/rrschedule.rb, line 258
def next_game_date(dt,wday)
  dt += 1 until wday == dt.wday && !self.exclude_dates.include?(dt)
  dt
end
reset_resource_availability() click to toggle source
# File lib/rrschedule.rb, line 303
def reset_resource_availability
  @gt_ps_avail = {}
  @cur_rule.gt.each do |gt|
    @gt_ps_avail[gt] = @cur_rule.ps.clone
  end
end
update_resource_availability(cur_gt,cur_ps) click to toggle source
# File lib/rrschedule.rb, line 310
def update_resource_availability(cur_gt,cur_ps)
  @gt_ps_avail[cur_gt].delete(cur_ps)
end
update_team_stats(game,cur_gt,cur_ps) click to toggle source
# File lib/rrschedule.rb, line 263
def update_team_stats(game,cur_gt,cur_ps)
  @stats[game.team_a][:gt][cur_gt] += 1
  @stats[game.team_a][:ps][cur_ps] += 1
  @stats[game.team_b][:gt][cur_gt] += 1
  @stats[game.team_b][:ps][cur_ps] += 1
end