slack-gamebot/models/match.rb
class Match
include Mongoid::Document
include Mongoid::Timestamps
SORT_ORDERS = ['created_at', '-created_at'].freeze
belongs_to :team, index: true
field :tied, type: Boolean, default: false
field :resigned, type: Boolean, default: false
field :scores, type: Array
belongs_to :challenge, index: true, optional: true
belongs_to :season, inverse_of: :matches, index: true, optional: true
before_create :calculate_elo!
after_create :update_users!
validate :validate_scores, unless: :tied?
validate :validate_tied_scores, if: :tied?
validate :validate_resigned_scores, if: :resigned?
validate :validate_tied_resigned
validates_presence_of :team
validate :validate_teams
has_and_belongs_to_many :winners, class_name: 'User', inverse_of: nil
has_and_belongs_to_many :losers, class_name: 'User', inverse_of: nil
# current matches are not in an archived season
scope :current, -> { where(season_id: nil) }
def scores?
scores&.any?
end
def to_s
if resigned?
"#{losers.map(&:display_name).and} resigned against #{winners.map(&:display_name).and}"
else
[
"#{winners.map(&:display_name).and} #{score_verb} #{losers.map(&:display_name).and}",
scores ? "with #{Score.scores_to_string(scores)}" : nil
].compact.join(' ')
end
end
def self.lose!(attrs)
Match.create!(attrs)
end
def self.resign!(attrs)
Match.create!(attrs.merge(resigned: true))
end
def self.draw!(attrs)
Match.create!(attrs.merge(tied: true))
end
def update_users!
if tied?
winners.inc(ties: 1)
losers.inc(ties: 1)
else
winners.inc(wins: 1)
losers.inc(losses: 1)
end
winners.each(&:calculate_streaks!)
losers.each(&:calculate_streaks!)
User.rank!(team)
end
def elo_s
winners_delta, losers_delta = calculated_elo
if (winners_delta | losers_delta).same?
winners_delta.first.to_i.to_s
elsif winners_delta.same? && losers_delta.same?
[winners_delta.first, losers_delta.first].map(&:to_i).and
else
(winners_delta | losers_delta).map(&:to_i).and
end
end
private
def validate_teams
teams = [team]
teams << challenge.team if challenge
teams.uniq!
errors.add(:team, 'Match can only be recorded for the same team.') if teams.count != 1
end
def validate_scores
return unless scores&.any?
errors.add(:scores, 'Loser scores must come first.') unless Score.valid?(scores)
end
def validate_tied_scores
return unless scores&.any?
errors.add(:scores, 'In a tie both sides must have the same number of points.') unless Score.tie?(scores)
end
def validate_resigned_scores
return unless scores&.any?
errors.add(:scores, 'Cannot score when resigning.')
end
def validate_tied_resigned
errors.add(:tied, 'Cannot be tied and resigned.') if tied? && resigned?
end
def score_verb
if tied?
'tied with'
elsif !scores
'defeated'
else
lose, win = Score.points(scores)
ratio = lose.to_f / win
if ratio > 0.9
'narrowly defeated'
elsif ratio > 0.4
'defeated'
else
'crushed'
end
end
end
def calculated_elo
@calcualted_elo ||= begin
winners_delta = []
losers_delta = []
winners_elo = Elo.team_elo(winners)
losers_elo = Elo.team_elo(losers)
losers_ratio = losers.any? ? [winners.size.to_f / losers.size, 1].min : 1
winners_ratio = winners.any? ? [losers.size.to_f / winners.size, 1].min : 1
ratio = if winners_elo == losers_elo && tied?
0 # no elo updates when tied and elo is equal
elsif tied?
0.5 # half the elo in a tie
else
1 # whole elo
end
winners.each do |winner|
e = 100 - (1.0 / (1.0 + (10.0**((losers_elo - winner.elo) / 400.0))) * 100)
winner.tau = [winner.tau + 0.5, Elo::MAX_TAU].min
delta = e * ratio * (Elo::DELTA_TAU**winner.tau) * winners_ratio
winners_delta << delta
winner.elo += delta
end
losers.each do |loser|
e = 100 - (1.0 / (1.0 + (10.0**((loser.elo - winners_elo) / 400.0))) * 100)
loser.tau = [loser.tau + 0.5, Elo::MAX_TAU].min
delta = e * ratio * (Elo::DELTA_TAU**loser.tau) * losers_ratio
losers_delta << delta
loser.elo -= delta
end
[losers_delta, winners_delta]
end
end
def calculate_elo!
calculated_elo
winners.each(&:save!)
losers.each(&:save!)
end
end