slack-gamebot/models/user.rb
class User
include Mongoid::Document
include Mongoid::Timestamps
field :user_id, type: String
field :user_name, type: String
field :wins, type: Integer, default: 0
field :losses, type: Integer, default: 0
field :losing_streak, type: Integer, default: 0
field :winning_streak, type: Integer, default: 0
field :ties, type: Integer, default: 0
field :elo, type: Integer, default: 0
field :elo_history, type: Array, default: []
field :tau, type: Float, default: 0
field :rank, type: Integer
field :captain, type: Boolean, default: false
field :registered, type: Boolean, default: true
field :nickname, type: String
belongs_to :team, index: true
validates_presence_of :team
index({ user_id: 1, team_id: 1 }, unique: true)
index(user_name: 1, team_id: 1)
index(wins: 1, team_id: 1)
index(losses: 1, team_id: 1)
index(ties: 1, team_id: 1)
index(elo: 1, team_id: 1)
before_save :update_elo_history!
after_save :rank!
SORT_ORDERS = ['elo', '-elo', 'created_at', '-created_at', 'wins', '-wins', 'losses', '-losses', 'ties', '-ties', 'user_name', '-user_name', 'rank', '-rank'].freeze
ANYONE = '*'.freeze
EVERYONE = %w[here channel].freeze
scope :ranked, -> { where(:rank.ne => nil) }
scope :captains, -> { where(captain: true) }
scope :everyone, -> { where(user_id: ANYONE) }
def current_matches
Match.current.where(team: team).any_of({ winner_ids: _id }, loser_ids: _id)
end
def slack_mention
anyone? ? 'anyone' : "<@#{user_id}>"
end
def display_name
registered ? nickname || user_name : '<unregistered>'
end
def anyone?
user_id == ANYONE
end
def self.slack_mention?(user_name)
slack_id = ::Regexp.last_match[1] if user_name =~ /^<[@!](.*)>$/
slack_id = ANYONE if slack_id && EVERYONE.include?(slack_id)
slack_id
end
def self.find_by_slack_mention!(client, user_name)
team = client.owner
slack_id = slack_mention?(user_name)
user = if slack_id
team.users.where(user_id: slack_id).first
else
regexp = ::Regexp.new("^#{user_name}$", 'i')
query = User.where(team: team).any_of({ user_name: regexp }, nickname: regexp)
query.first
end
unless user
case slack_id
when ANYONE
user = User.create!(team: team, user_id: ANYONE, user_name: ANYONE, nickname: 'anyone', registered: true)
else
begin
users_info = client.web_client.users_info(user: slack_id || "@#{user_name}")
if users_info
info = Hashie::Mash.new(users_info).user
if info
user = team.users.where(user_id: info.id).first
user ||= User.create!(team: team, user_id: info.id, user_name: info.name, registered: true)
end
end
rescue Slack::Web::Api::Errors::SlackError => e
raise e unless e.message == 'user_not_found'
end
end
end
raise SlackGamebot::Error, "I don't know who #{user_name} is! Ask them to _register_." unless user&.registered?
raise SlackGamebot::Error, "I don't know who #{user_name} is!" unless user
user
end
def self.find_many_by_slack_mention!(client, user_names)
user_names.map { |user| find_by_slack_mention!(client, user) }
end
# Find an existing record, update the username if necessary, otherwise create a user record.
def self.find_create_or_update_by_slack_id!(client, slack_id)
instance = User.where(team: client.owner, user_id: slack_id).first
users_info = client.web_client.users_info(user: slack_id)
instance_info = Hashie::Mash.new(users_info).user if users_info
if users_info && instance
instance.update_attributes!(user_name: instance_info.name) if instance.user_name != instance_info.name
elsif !instance && instance_info
instance = User.create!(team: client.owner, user_id: slack_id, user_name: instance_info.name, registered: true)
end
if instance
instance.register! unless instance.registered?
instance.promote! unless instance.captain? || client.owner.captains.count > 0
end
raise SlackGamebot::Error, "I don't know who <@#{slack_id}> is!" unless instance
instance
end
def self.reset_all!(team)
User.where(team: team).set(
wins: 0,
losses: 0,
ties: 0,
elo: 0,
elo_history: [],
tau: 0,
rank: nil,
losing_streak: 0,
winning_streak: 0
)
end
def to_s
wins_s = "#{wins} win#{wins == 1 ? '' : 's'}"
losses_s = "#{losses} loss#{losses == 1 ? '' : 'es'}"
ties_s = "#{ties} tie#{ties == 1 ? '' : 's'}" if ties && ties > 0
elo_s = "elo: #{team_elo}"
lws_s = "lws: #{winning_streak}" if winning_streak >= losing_streak && winning_streak >= 3
lls_s = "lls: #{losing_streak}" if losing_streak > winning_streak && losing_streak >= 3
"#{display_name}: #{[wins_s, losses_s, ties_s].compact.join(', ')} (#{[elo_s, lws_s, lls_s].compact.join(', ')})"
end
def team_elo
elo + team.elo
end
def promote!
update_attributes!(captain: true)
end
def demote!
update_attributes!(captain: false)
end
def register!
return if registered?
update_attributes!(registered: true)
User.rank!(team)
end
def unregister!
return unless registered?
update_attributes!(registered: false, rank: nil)
User.rank!(team)
end
def rank!
return unless elo_changed?
User.rank!(team)
reload.rank
end
def update_elo_history!
return unless elo_changed?
elo_history << elo
end
def self.rank!(team)
rank = 1
players = any_of({ :wins.gt => 0 }, { :losses.gt => 0 }, :ties.gt => 0).where(team: team, registered: true).desc(:elo).desc(:wins).asc(:losses).desc(:ties)
players.each_with_index do |player, index|
if player.registered?
rank += 1 if index > 0 && %i[elo wins losses ties].any? { |property| players[index - 1].send(property) != player.send(property) }
player.set(rank: rank) unless rank == player.rank
end
end
end
def calculate_streaks!
longest_winning_streak = 0
longest_losing_streak = 0
current_winning_streak = 0
current_losing_streak = 0
current_matches.asc(:_id).each do |match|
if match.tied?
current_winning_streak = 0
current_losing_streak = 0
elsif match.winner_ids.include?(_id)
current_losing_streak = 0
current_winning_streak += 1
else
current_winning_streak = 0
current_losing_streak += 1
end
longest_losing_streak = current_losing_streak if current_losing_streak > longest_losing_streak
longest_winning_streak = current_winning_streak if current_winning_streak > longest_winning_streak
end
return if losing_streak == longest_losing_streak && winning_streak == longest_winning_streak
update_attributes!(losing_streak: longest_losing_streak, winning_streak: longest_winning_streak)
end
def self.rank_section(team, users)
ranks = users.map(&:rank)
return users unless ranks.min && ranks.max
where(team: team, :rank.gte => ranks.min, :rank.lte => ranks.max).asc(:rank).asc(:wins).asc(:ties)
end
end