├── .rspec ├── lib ├── elo_demo │ └── version.rb └── elo_demo.rb ├── Gemfile ├── spec ├── spec_helper.rb └── elo_demo_spec.rb ├── .travis.yml ├── .gitignore ├── Rakefile ├── bin ├── setup ├── console └── run_demo ├── LICENSE.txt ├── elo_demo.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/elo_demo/version.rb: -------------------------------------------------------------------------------- 1 | module EloDemo 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.3.1' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "elo_demo" 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: gem install bundler -v 1.13.1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /spec/elo_demo_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe EloDemo do 4 | it "has a version number" do 5 | expect(EloDemo::VERSION).not_to be nil 6 | end 7 | 8 | it "does something useful" do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "elo_demo" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 TODO: Write your name 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /elo_demo.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'elo_demo/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "elo_demo" 8 | spec.version = EloDemo::VERSION 9 | spec.authors = ["Fabio Akita"] 10 | spec.email = ["boss@akitaonrails.com"] 11 | 12 | spec.summary = %q{This is a short demonstration of one of the most miunderstood topic in programming: ranking} 13 | spec.description = %q{This short demo will show what most people think of ranking and what the beginnings of a good ranking system actually look like} 14 | spec.homepage = "https://github.com/akitaonrails/elo_demo" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_runtime_dependency "elo", "~> 0.1.0" 25 | 26 | spec.add_development_dependency "bundler", "~> 1.13" 27 | spec.add_development_dependency "rake", "~> 10.0" 28 | spec.add_development_dependency "rspec", "~> 3.0" 29 | end 30 | -------------------------------------------------------------------------------- /bin/run_demo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "elo_demo" 5 | 6 | puts "##" 7 | puts "## NAIVE RUN" 8 | 9 | t = EloDemo::Tournment.new 10 | t.run! 11 | t.print_naive_ranking 12 | 13 | attempts = 10 14 | 15 | second_player = t.sorted_by_naive[1] 16 | third_player = t.sorted_by_naive[2] 17 | 18 | puts "#{second_player.name} (2nd) wins from #{third_player.name} (3rd) #{attempts} times" 19 | attempts.times do 20 | t.mark_win(second_player, third_player) 21 | end 22 | 23 | t.print_naive_ranking 24 | 25 | second_player = t.sorted_by_naive[1] 26 | worst_player = t.sorted_by_naive.last 27 | 28 | puts "#{second_player.name} (2nd) loses to #{worst_player.name} (10th) #{attempts} times" 29 | attempts.times do 30 | t.mark_win(worst_player, second_player) 31 | end 32 | 33 | t.print_naive_ranking 34 | 35 | fourth_player = t.sorted_by_naive[3] 36 | fifth_player = t.sorted_by_naive[4] 37 | 38 | puts "#{fourth_player.name} (4th) wins to #{fifth_player.name} (5th) #{attempts} times" 39 | attempts.times do 40 | t.mark_win(fourth_player, fifth_player) 41 | end 42 | 43 | t.print_naive_ranking 44 | 45 | 46 | 47 | 48 | puts "##" 49 | puts "## ELO RUN" 50 | 51 | t = EloDemo::Tournment.new 52 | t.run! 53 | t.print_elo_ranking 54 | 55 | attempts = 10 56 | 57 | second_player = t.sorted_by_elo[1] 58 | third_player = t.sorted_by_elo[2] 59 | 60 | puts "#{second_player.name} (2nd) wins from #{third_player.name} (3rd) #{attempts} times" 61 | attempts.times do 62 | t.mark_win(second_player, third_player) 63 | end 64 | 65 | t.print_elo_ranking 66 | 67 | second_player = t.sorted_by_elo[1] 68 | worst_player = t.sorted_by_elo.last 69 | 70 | puts "#{second_player.name} (2nd) loses to #{worst_player.name} (10th) #{attempts} times" 71 | attempts.times do 72 | t.mark_win(worst_player, second_player) 73 | end 74 | 75 | t.print_elo_ranking 76 | 77 | fourth_player = t.sorted_by_elo[3] 78 | fifth_player = t.sorted_by_elo[4] 79 | 80 | puts "#{fourth_player.name} (4th) wins to #{fifth_player.name} (5th) #{attempts} times" 81 | attempts.times do 82 | t.mark_win(fourth_player, fifth_player) 83 | end 84 | 85 | t.print_elo_ranking 86 | -------------------------------------------------------------------------------- /lib/elo_demo.rb: -------------------------------------------------------------------------------- 1 | require "elo_demo/version" 2 | require "elo" 3 | 4 | module EloDemo 5 | class Player 6 | attr_accessor :name, :games_played, :wins, :loses, :elo_player 7 | def initialize(options = {}) 8 | self.name = options[:name] 9 | self.games_played = options[:games_played] || 0 10 | self.wins = options[:wins] || 0 11 | self.loses = options[:loses] || 0 12 | self.elo_player = Elo::Player.new 13 | end 14 | end 15 | 16 | class Tournment 17 | attr_reader :players 18 | NAMES = %w[Mario Luigi Zelda Bowser Yoshi Wario Fox Pikachu Kong Samus Kirby] 19 | 20 | def initialize 21 | srand(666) 22 | 23 | @players = [] 24 | 10.times do |i| 25 | @players << Player.new(name: NAMES[i]) 26 | end 27 | end 28 | 29 | def run! 30 | 1000.times do 31 | player_0 = @players[rand(1..players.size) - 1] 32 | player_1 = @players[rand(1..players.size) - 1] 33 | game = [player_0, player_1] 34 | winner = game[rand(0..1)] 35 | 36 | if player_0.name == winner.name 37 | mark_win(player_0, player_1) 38 | else 39 | mark_win(player_1, player_0) 40 | end 41 | end 42 | end 43 | 44 | def mark_win(p1, p2) 45 | p1.games_played += 1 46 | p2.games_played += 1 47 | p1.wins += 1 48 | p2.loses += 1 49 | p1.elo_player.wins_from(p2.elo_player) 50 | end 51 | 52 | def print_ranking 53 | print_naive_ranking 54 | print_elo_ranking 55 | end 56 | 57 | def print_naive_ranking 58 | puts "Ranking sorted by Points (wins - loses)" 59 | puts " #{"Name".ljust(10)} Games Wins Loses Points Elo Rating" 60 | sorted_by_naive.each_with_index do |p, index| 61 | print_line p, index + 1 62 | end 63 | puts "" 64 | end 65 | 66 | def print_elo_ranking 67 | puts "Ranking sorted by Elo Rating" 68 | puts " #{"Name".ljust(10)} Games Wins Loses Points Elo Rating" 69 | sorted_by_elo.each_with_index do |p, index| 70 | print_line p, index + 1 71 | end 72 | puts "" 73 | end 74 | 75 | def sorted_by_naive 76 | players.sort_by { |p| p.wins - p.loses }.reverse 77 | end 78 | 79 | def sorted_by_elo 80 | players.sort_by { |p| p.elo_player.rating }.reverse 81 | end 82 | 83 | def print_line(p, index) 84 | puts "#{index.to_s.rjust(3)} #{p.name.ljust(10)} #{p.games_played.to_s.rjust(5)} #{p.wins.to_s.rjust(4)} #{p.loses.to_s.rjust(5)} #{(p.wins - p.loses).to_s.rjust(5)} #{p.elo_player.rating.to_s.rjust(10)}" 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elo Rating Demonstration 2 | 3 | Most people do ranking wrong. In this small demonstration I have 10 players battling each other through 1,000 random matches. 4 | 5 | The most naive sorting would be to create a simple score (such as number of wins subtracted by number of loses). In this case, this is the simple ranking: 6 | 7 | ``` 8 | Ranking sorted by Points (wins - loses) 9 | Name Games Wins Loses Points Elo Rating 10 | 1 Kong 217 117 100 17 802 11 | 2 Samus 211 110 101 9 842 12 | 3 Wario 197 102 95 7 824 13 | 4 Luigi 186 95 91 4 841 14 | 5 Zelda 160 81 79 2 847 15 | 6 Pikachu 209 105 104 1 851 16 | 7 Yoshi 223 112 111 1 803 17 | 8 Mario 203 101 102 -1 820 18 | 9 Fox 208 95 113 -18 754 19 | 10 Bowser 186 82 104 -22 785 20 | ``` 21 | 22 | Now we make Samus (2nd) wins from Wario (3rd) 10 times in a row, without no one else battling this time. 23 | 24 | ``` 25 | Ranking sorted by Points (wins - loses) 26 | Name Games Wins Loses Points Elo Rating 27 | 1 Samus 221 120 101 19 896 28 | 2 Kong 217 117 100 17 802 29 | 3 Luigi 186 95 91 4 841 30 | 4 Zelda 160 81 79 2 847 31 | 5 Pikachu 209 105 104 1 851 32 | 6 Yoshi 223 112 111 1 803 33 | 7 Mario 203 101 102 -1 820 34 | 8 Wario 207 102 105 -3 760 35 | 9 Fox 208 95 113 -18 754 36 | 10 Bowser 186 82 104 -22 785 37 | ``` 38 | 39 | You can see that Samus went to the top of the ranking while poor Wario drastically dropped from 3rd to 8th place. 40 | 41 | This is very unfair as Wario was already weaker than then Samus so the odds of him winning were not high to begin with, and the penalty of trying against a stronger opponent made him drop a lot in the ranking. 42 | 43 | Let's try to make Kong (2nd) losing to Bowser (10th) 10 times. 44 | 45 | ``` 46 | Ranking sorted by Points (wins - loses) 47 | Name Games Wins Loses Points Elo Rating 48 | 1 Samus 221 120 101 19 896 49 | 2 Kong 227 117 110 7 732 50 | 3 Luigi 186 95 91 4 841 51 | 4 Zelda 160 81 79 2 847 52 | 5 Pikachu 209 105 104 1 851 53 | 6 Yoshi 223 112 111 1 803 54 | 7 Mario 203 101 102 -1 820 55 | 8 Wario 207 102 105 -3 760 56 | 9 Bowser 196 92 104 -12 845 57 | 10 Fox 208 95 113 -18 754 58 | ``` 59 | 60 | Now even though Kong was supposed to have better odds for being stronger, he lost 10 times in a row but he's still at 2nd place. 61 | 62 | And Bowser, who performed an impressive winning streak of 10 wins agains the 2nd most stronger, still only jumped up one position in the ranking, to 9th place. 63 | 64 | Again, this is very unfair. And this is why simple counting methods such as absolute ammout of wins and loses are not used in real rankings. 65 | 66 | 67 | ## Elo Rating Run 68 | 69 | Starting over with the exact 1,000 matches in the exact same order and win x lose scenario, we already arrive to a very different ranking than the first one: 70 | 71 | In the naive system, the current top player Pikachu was only in 6th, and Kong, who is at 8th here was considered the top player. 72 | 73 | ``` 74 | Ranking sorted by Elo Rating 75 | Name Games Wins Loses Points Elo Rating 76 | 1 Pikachu 209 105 104 1 851 77 | 2 Zelda 160 81 79 2 847 78 | 3 Samus 211 110 101 9 842 79 | 4 Luigi 186 95 91 4 841 80 | 5 Wario 197 102 95 7 824 81 | 6 Mario 203 101 102 -1 820 82 | 7 Yoshi 223 112 111 1 803 83 | 8 Kong 217 117 100 17 802 84 | 9 Bowser 186 82 104 -22 785 85 | 10 Fox 208 95 113 -18 754 86 | ``` 87 | 88 | Now let's make Zelda (2nd) win from Samus (3rd) 10 times. 89 | 90 | They are almost evenly matched (almost same level in the ranking), so Zelda jumps to 1st place and Samus drops from 3rd to 9th. 91 | 92 | ``` 93 | Ranking sorted by Elo Rating 94 | Name Games Wins Loses Points Elo Rating 95 | 1 Zelda 170 91 79 12 904 96 | 2 Pikachu 209 105 104 1 851 97 | 3 Luigi 186 95 91 4 841 98 | 4 Wario 197 102 95 7 824 99 | 5 Mario 203 101 102 -1 820 100 | 6 Yoshi 223 112 111 1 803 101 | 7 Kong 217 117 100 17 802 102 | 8 Bowser 186 82 104 -22 785 103 | 9 Samus 221 110 111 -1 775 104 | 10 Fox 208 95 113 -18 754 105 | ``` 106 | 107 | Now let's try the improbable scenario of Pikachu (2nd) losing to Fox (10th) 10 times. 108 | 109 | ``` 110 | Ranking sorted by Elo Rating 111 | Name Games Wins Loses Points Elo Rating 112 | 1 Zelda 170 91 79 12 904 113 | 2 Luigi 186 95 91 4 841 114 | 3 Fox 218 105 113 -8 829 115 | 4 Wario 197 102 95 7 824 116 | 5 Mario 203 101 102 -1 820 117 | 6 Yoshi 223 112 111 1 803 118 | 7 Kong 217 117 100 17 802 119 | 8 Bowser 186 82 104 -22 785 120 | 9 Samus 221 110 111 -1 775 121 | 10 Pikachu 219 105 114 -9 766 122 | ``` 123 | 124 | They almost swapped positions because by the skill reflected in the ranking, Pikachu had a higher probability of winning against a 'weak' opponent such as Fox. 125 | 126 | But instead, Fox did an impressive winning streak, so he deserved jumping up from 10th to 3rd, and Pikachu suffered a severe penalty of dropping from 3rd to 10th for losing so many times. 127 | 128 | This is a much fair system where we use the probability of wins and loses. 129 | 130 | If a strong player does a match against a weaker player, he shouldn't jump up too much in the ranking while the weaker shouldn't drop down so much from the ranking, as it's all expected outcomes. 131 | 132 | Now, in the case of improbable matches where a stronger player loses against a weaker player, the stronger should drop down a lot more and the weaker should jump up a lot more as reward. 133 | 134 | This motivates the stronger player to play their best to stay up and the weaker to risk against stronger opponents to wield better rewards. 135 | 136 | That's what makes a more competitive environment for players. 137 | 138 | ## Installation 139 | 140 | And then execute: 141 | 142 | $ bundle 143 | 144 | Or install it yourself as: 145 | 146 | $ gem install elo_demo 147 | 148 | ## Usage 149 | 150 | Just execute the scenario runner: 151 | 152 | $ bin/run_demo 153 | 154 | ## Contributing 155 | 156 | Bug reports and pull requests are welcome on GitHub at https://github.com/akitaonrails/elo_demo. 157 | 158 | 159 | ## License 160 | 161 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 162 | 163 | --------------------------------------------------------------------------------