0.0
The project is in a healthy, maintained state
Adds Elo rating to any ActiveRecord model via has_elo_ranking. It stores ratings in a separate EloRanking model to keep your host model clean, and provides domain-style methods for updating rankings after matches.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 6.0, < 9.0
>= 6.0, < 9.0
 Project Readme

elo_rankable

Gem Version

A Ruby gem that adds Elo rating capabilities to any ActiveRecord model using a simple has_elo_ranking declaration. It stores ratings in a separate EloRanking model to keep your host model clean, and provides domain-style methods for updating rankings after matches.

Features

  • ๐ŸŽฏ Simple Integration: Add Elo rankings to any ActiveRecord model with one line
  • ๐Ÿ† Multiple Match Types: Support for 1v1, draws, multiplayer (ranked), and winner-vs-all matches
  • โš™๏ธ Configurable: Customizable base rating and K-factor strategies
  • ๐Ÿ“Š Leaderboards: Built-in scopes for rankings and top players
  • ๐Ÿงน Clean Design: Ratings stored separately from your main models
  • ๐Ÿ”„ Polymorphic: Works with any ActiveRecord model (User, Player, Team, etc.)

Installation

Add this line to your application's Gemfile:

gem 'elo_rankable'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install elo_rankable

Setup

1. Generate and run the migration

$ rails generate elo_rankable:install
$ rails db:migrate

2. Add to your models

class Player < ApplicationRecord
  has_elo_ranking
end

class Team < ApplicationRecord
  has_elo_ranking
end

Usage

Basic 1v1 Matches

alice = Player.create!(name: "Alice")
bob = Player.create!(name: "Bob")

# Both players start with the default rating (1200)
alice.elo_rating  # => 1200
bob.elo_rating    # => 1200

# Record a match where Alice beats Bob
alice.beat!(bob)

alice.elo_rating  # => 1216
bob.elo_rating    # => 1184

# Alternative syntax
bob.lost_to!(alice)  # Same effect as alice.beat!(bob)

# Record a draw
alice.draw_with!(bob)

alice.elo_rating  # => 1200 (no change for equal ratings)
bob.elo_rating    # => 1200 (no change for equal ratings)

# Example with different ratings
charlie = Player.create!(name: "Charlie")
charlie.elo_ranking.update!(rating: 1400)  # Charlie is higher rated

charlie.draw_with!(alice)  # Draw between 1400 vs 1200

charlie.elo_rating  # => 1392 (lost 8 points - draw hurts higher rated player)
alice.elo_rating    # => 1208 (gained 8 points - draw helps lower rated player)

Multiplayer Matches (Ranked)

For tournaments or matches where players finish in ranked order:

players = [first_place, second_place, third_place, fourth_place]

# Higher-indexed players are treated as having lost to lower-indexed ones
EloRankable.record_multiplayer_match(players)

# This is equivalent to:
# first_place.beat!(second_place)
# first_place.beat!(third_place)
# first_place.beat!(fourth_place)
# second_place.beat!(third_place)
# second_place.beat!(fourth_place)
# third_place.beat!(fourth_place)

Winner vs All Matches

For matches where one player/team beats everyone else, but the losers don't compete against each other:

winner = Player.find_by(name: "Champion")
losers = [player1, player2, player3]

EloRankable.record_winner_vs_all(winner, losers)

# Winner gains rating by beating each loser individually
# Losers only lose rating to the winner, not to each other

Global Draw Recording

EloRankable.record_draw(player1, player2)

Accessing Rating Information

player = Player.first

player.elo_rating      # Current Elo rating
player.games_played    # Number of games played
player.elo_ranking     # Access to the full EloRanking record

Accessing K-Factor Values

# Get the K-factor for a specific rating
EloRankable.config.k_factor_for(1500)  # => 32
EloRankable.config.k_factor_for(2200)  # => 20

Leaderboards and Scopes

# Get players ordered by rating (highest first)
top_players = Player.by_elo_rating

# Get top 10 players
top_10 = Player.top_rated(10)

# Access EloRanking records directly
top_ratings = EloRankable::EloRanking.by_rating.limit(10)

Configuration

Base Rating

EloRankable.configure do |config|
  config.base_rating = 1500  # Default is 1200
end

K-Factor Strategy

The K-factor determines how much ratings change after each match. You can use a constant value or a dynamic strategy based on rating:

Constant K-Factor

EloRankable.configure do |config|
  config.k_factor_for = 32
end

Dynamic K-Factor (Default)

EloRankable.configure do |config|
  config.k_factor_for = ->(rating) do
    if rating > 2400
      10   # Masters: smaller changes
    elsif rating > 2000
      20   # Experts: medium changes  
    else
      32   # Beginners: larger changes
    end
  end
end

Method Reference

Instance Methods (added by has_elo_ranking)

Method Description
beat!(other) Record a win against another player
lost_to!(other) Record a loss to another player
draw_with!(other) Record a draw with another player
elo_beat!(other) Alias for beat!
elo_lost_to!(other) Alias for lost_to!
elo_draw_with!(other) Alias for draw_with!
elo_rating Current Elo rating
games_played Number of games played
elo_ranking Associated EloRanking record

Class Methods (added by has_elo_ranking)

Scope Description
by_elo_rating Order by Elo rating (highest first)
top_rated(limit) Get top N players by rating

Module Methods

Method Description
EloRankable.record_multiplayer_match(players) Record ranked multiplayer match
EloRankable.record_winner_vs_all(winner, losers) Record winner-takes-all match
EloRankable.record_draw(player1, player2) Record a draw

How Elo Rating Works

The Elo rating system calculates expected outcomes based on rating differences and adjusts ratings based on actual results:

  • Expected Score: Higher-rated players are expected to win more often
  • Rating Change: Beating a higher-rated opponent gives more points than beating a lower-rated one
  • K-Factor: Controls how much ratings can change (higher K = more volatile)

Example Calculation

# Alice (1200) vs Bob (1200) - equal ratings
alice.beat!(bob)
# Alice: 1200 + 16 = 1216 (gained 16 points)
# Bob:   1200 - 16 = 1184 (lost 16 points)

# Alice (1400) vs Charlie (1200) - Alice favored
alice.beat!(charlie)
# Alice: 1400 + 8 = 1408 (gained 8 points - expected to win)
# Charlie: 1200 - 8 = 1192 (lost 8 points)

# Charlie (1192) beats Alice (1408) - upset!
charlie.beat!(alice)
# Charlie: 1192 + 24 = 1216 (gained 24 points - major upset)
# Alice: 1408 - 24 = 1384 (lost 24 points)

Error Handling

The gem provides comprehensive validation with specific error types:

InvalidMatchError

  • Thrown when match requirements aren't met (e.g., less than 2 players)
  • Winner appears in losers list

ArgumentError

  • Nil players/opponents
  • Duplicate players in arrays
  • Players that don't respond to elo_ranking
  • Playing against yourself or destroyed records
# Examples that will raise errors:
alice.beat!(nil)                    # ArgumentError: Cannot play against nil
alice.beat!(alice)                  # ArgumentError: Cannot play against yourself
EloRankable.record_multiplayer_match([alice])  # InvalidMatchError: Need at least 2 players

Database Schema

The gem creates an elo_rankings table:

create_table :elo_rankings do |t|
  t.references :rankable, polymorphic: true, null: false, index: true
  t.integer :rating, null: false, default: 1200
  t.integer :games_played, null: false, default: 0
  t.timestamps
end

add_index :elo_rankings, :rating
add_index :elo_rankings, [:rankable_type, :rankable_id], unique: true

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/aberen/elo_rankable.

License

The gem is available as open source under the terms of the MIT License.