From dc4278d62341e15fb5031e940af4000c8677fc96 Mon Sep 17 00:00:00 2001 From: Marius Gavrilescu Date: Wed, 30 Nov 2016 23:44:13 +0000 Subject: [PATCH 1/1] Initial commit --- Changes | 4 + MANIFEST | 6 ++ Makefile.PL | 21 ++++ README | 36 +++++++ lib/Games/Ratings/LogisticElo.pm | 159 +++++++++++++++++++++++++++++++ t/Games-Ratings-LogisticElo.t | 44 +++++++++ 6 files changed, 270 insertions(+) create mode 100644 Changes create mode 100644 MANIFEST create mode 100644 Makefile.PL create mode 100644 README create mode 100644 lib/Games/Ratings/LogisticElo.pm create mode 100644 t/Games-Ratings-LogisticElo.t diff --git a/Changes b/Changes new file mode 100644 index 0000000..87f79d3 --- /dev/null +++ b/Changes @@ -0,0 +1,4 @@ +Revision history for Perl extension Games::Ratings::Elo. + +0.001 2016-11-30T23:44+00:00 + - Initial release diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..ac2c65b --- /dev/null +++ b/MANIFEST @@ -0,0 +1,6 @@ +Changes +Makefile.PL +MANIFEST +README +t/Games-Ratings-LogisticElo.t +lib/Games/Ratings/LogisticElo.pm diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..7e80656 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,21 @@ +use 5.014000; +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => 'Games::Ratings::LogisticElo', + VERSION_FROM => 'lib/Games/Ratings/LogisticElo.pm', + ABSTRACT_FROM => 'lib/Games/Ratings/LogisticElo.pm', + AUTHOR => 'Marius Gavrilescu ', + MIN_PERL_VERSION => '5.14.0', + LICENSE => 'perl', + SIGN => 1, + PREREQ_PM => { + qw/Games::Ratings 0/, + }, + META_ADD => { + dynamic_config => 0, + resources => { + repository => 'https://git.ieval.ro/?p=games-ratings-logisticelo.git', + }, + } +); diff --git a/README b/README new file mode 100644 index 0000000..0f7c24e --- /dev/null +++ b/README @@ -0,0 +1,36 @@ +Games-Ratings-LogisticElo version 0.001 +=============================== + +Games-Ratings-LogisticElo provides methods to calculate Elo rating +changes. Unlike L, this Elo +implementation uses the logistic distribution instead of the standard +distribution. + +Games-Ratings-LogisticElo can be used both for a single player who +played multiple rated games, and for a single competition with an +arbitrary number of players. + +INSTALLATION + +To install this module type the following: + + perl Makefile.PL + make + make test + make install + +DEPENDENCIES + +This module requires these other modules and libraries: + +* Games::Ratings + +COPYRIGHT AND LICENCE + +Copyright (C) 2016 by Marius Gavrilescu + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.22.2 or, +at your option, any later version of Perl 5 you may have available. + + diff --git a/lib/Games/Ratings/LogisticElo.pm b/lib/Games/Ratings/LogisticElo.pm new file mode 100644 index 0000000..25e4e6d --- /dev/null +++ b/lib/Games/Ratings/LogisticElo.pm @@ -0,0 +1,159 @@ +package Games::Ratings::LogisticElo; + +use 5.014000; +use strict; +use warnings; +use parent qw/Exporter Games::Ratings/; + +our @EXPORT_OK = qw/multi_elo/; +our @EXPORT = qw//; +our $VERSION = '0.001'; + +use List::Util qw/sum/; + +sub get_rating_change { + my ($self) = @_; + + my $own_rating = $self->get_rating; + my $K = $self->get_coefficient; + + my $expected = sum map { + my $exp = ($_->{opponent_rating} - $own_rating) / 400; + 1 / (1 + 10 ** $exp) + } $self->get_all_games; + + my $actual = sum map { + Games::Ratings::_get_numerical_result($_->{result}) + } $self->get_all_games; + + $K * ($actual - $expected) +} + +sub get_new_rating { + my ($self) = @_; + $self->get_rating + $self->get_rating_change +} + +sub multi_elo { + my @args = @_; + my $K = ref $args[0] ? 15 : shift @args; + + my @newratings = map { + my $player = __PACKAGE__->new; + $player->set_rating($_->[0]); + $player->set_coefficient($K); + for my $opponent (@args) { + $player->add_game({ + opponent_rating => $opponent->[0], + result => + $_->[1] > $opponent->[1] ? 'win' : + $_->[1] < $opponent->[1] ? 'loss' : 'draw' + }) + } + $player->get_new_rating + } @args; + + wantarray ? @newratings : \@newratings +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Games::Ratings::LogisticElo - calculate changes to logistic curve Elo ratings + +=head1 SYNOPSIS + + use Games::Ratings::LogisticElo; + my $player = Games::Ratings::LogisticElo->new; + $player->set_rating(2240); + $player->set_coefficient(15); + $player->add_game({ + opponent_rating => 2114, + result => 'win', ## or 'draw' or 'loss' + }); + say 'Rating change: ' . $player->get_rating_change; + say 'New rating: ' . $player->get_new_rating; + + use Games::Ratings::LogisticElo qw/multi_elo/; + my @results = [2240, 3], [2114, 2], [2300, 1]; + my @new_ratings = multi_elo 15, @results; + say 'Rating changes for this comp: ', join ', ', + map { $new_ratings[$_] - $results[$_]->[0] } 0 .. $#results; + +=head1 DESCRIPTION + +This module provides methods to calculate Elo rating changes. Unlike +L, this Elo implementation uses the +logistic distribution instead of the standard distribution. + +This module can be used both for a single player who played multiple +rated games, and for a single competition with an arbitrary number of +players. + +=head1 FUNCTIONS + +Games::Ratings::LogisticElo inherits from L, see that +module's documentation for information about the inherited methods. + +Nothing is exported by default, the function B can be +exported on request. + +=over + +=item B<$self>->I + +Computes and returns how much a player's rating changes after the +games added. + +=item B<$self>->I + +Adds the result of I to the old rating of the +player and returns this. + +=item B [$coefficient], @results + +Computes the ratings after a competition with an arbitrary number of +players. + +The first argument is the coefficient. It is optional, with the +default coefficient being 15. The next arguments are the results of +the players. Each result is a 2-element arrayref, the first element +being the Elo rating of the player, and the second element being the +score that player obtained. The scores are only used to compare +players, their absolute values are irrelevant. + +The return value is a list (in list context) or arrayref (in scalar +context) of ratings of all players after the competition, in the same +order as the arguments. + +This function computes the ratings by considering that each player +played a game with every other player, with the winner of every game +being the player who got the highest score. + +=back + + +=head1 SEE ALSO + +L, L + +L + +=head1 AUTHOR + +Marius Gavrilescu + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2016 by Marius Gavrilescu + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.22.2 or, +at your option, any later version of Perl 5 you may have available. + + +=cut diff --git a/t/Games-Ratings-LogisticElo.t b/t/Games-Ratings-LogisticElo.t new file mode 100644 index 0000000..e256010 --- /dev/null +++ b/t/Games-Ratings-LogisticElo.t @@ -0,0 +1,44 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use Test::More tests => 7; +BEGIN { use_ok('Games::Ratings::LogisticElo', 'multi_elo') }; + +my $player = Games::Ratings::LogisticElo->new; +$player->set_rating(2240); +$player->set_coefficient(15); +$player->add_game({ + opponent_rating => 2114, + result => 'win', +}); +my $rating_change = int ($player->get_rating_change + 0.5); +my $new_rating = int ($player->get_new_rating + 0.5); + +is $rating_change, 5, 'rating change'; +is $new_rating, 2245, 'new rating'; + + +sub clean_ratings { + map { int ($_ + 0.5) } @_ +} + +my @ratings = clean_ratings multi_elo [2240, 5]; +is_deeply [@ratings], [2240], 'multi_elo - 1 arg'; + +@ratings = clean_ratings multi_elo [2240, 5], [2114, 2]; +is_deeply [@ratings], [2245, 2109], 'multi_elo - 2 args'; + +@ratings = clean_ratings multi_elo 30, [2114, 2], [2240, 5]; +is_deeply [@ratings], [2104, 2250], 'multi_elo - 2 args, custom K'; + + +$player = Games::Ratings::LogisticElo->new; +$player->set_rating(2240); +$player->set_coefficient(15); +$player->add_game({ + opponent_rating => 2240, + result => 'draw', +}) for 1 .. 100; +$rating_change = int ($player->get_rating_change + 0.5); +is $rating_change, 0, 'rating change (only draws against self)'; -- 2.30.2