├── .gitignore ├── .gitmodules ├── LICENSE ├── README ├── docs ├── conf.py └── index.rst ├── elo.py ├── elopopulars.py ├── elotests.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | # Translations 24 | *.mo 25 | 26 | # Mr Developer 27 | .mr.developer.cfg 28 | 29 | # Vim 30 | .*.sw[op] 31 | 32 | # Testing 33 | .cache 34 | *$py.class 35 | 36 | # Shinx 37 | docs/Makefile 38 | docs/_build 39 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git@github.com:sublee/sublee-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 by Heungsub Lee. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Elo, the classic rating system 2 | 3 | by Heungsub Lee 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Elo documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Mar 26 23:27:52 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('_themes')) 20 | sys.path.insert(0, os.path.abspath('..')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.mathjax'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Elo' 45 | copyright = u'2013, Heungsub Lee' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | from elo import __version__ as version 53 | # The full version, including alpha/beta/rc tags. 54 | release = version 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'elo' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | html_theme_options = {'github_fork': 'sublee/elo', 101 | 'google_analytics': 'UA-28655602-1'} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | html_theme_path = ['_themes'] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | html_favicon = 'favicon.ico' 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'elodoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | latex_elements = { 175 | # The paper size ('letterpaper' or 'a4paper'). 176 | #'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'elo.tex', u'Elo Documentation', 189 | u'Heungsub Lee', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'elo', u'Elo Documentation', 219 | [u'Heungsub Lee'], 1) 220 | ] 221 | 222 | # If true, show URL addresses after external links. 223 | #man_show_urls = False 224 | 225 | 226 | # -- Options for Texinfo output ------------------------------------------------ 227 | 228 | # Grouping the document tree into Texinfo files. List of tuples 229 | # (source start file, target name, title, author, 230 | # dir menu entry, description, category) 231 | texinfo_documents = [ 232 | ('index', 'elo', u'Elo Documentation', 233 | u'Heungsub Lee', 'Elo', 'One line description of project.', 234 | 'Miscellaneous'), 235 | ] 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #texinfo_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #texinfo_domain_indices = True 242 | 243 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 244 | #texinfo_show_urls = 'footnote' 245 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Elo 2 | === 3 | 4 | the classic rating system 5 | 6 | .. currentmodule:: elo 7 | 8 | What's Elo? 9 | ~~~~~~~~~~~ 10 | 11 | Elo_ is the most famous rating system among game players. It was invented by 12 | `Arpad Elo`_ for chess tournaments of USCF_. :: 13 | 14 | from elo import Rating, quality_1vs1, rate_1vs1 15 | alice, bob = Rating(1000), Rating(1400) # assign Alice and Bob's ratings 16 | if quality_1vs1(alice, bob) < 0.50: 17 | print('This match seems to be not so fair') 18 | alice, bob = rate_1vs1(alice, bob) # update the ratings after the match 19 | 20 | .. _Elo: http://en.wikipedia.org/wiki/Elo_rating_system 21 | .. _Arpad Elo: http://en.wikipedia.org/wiki/Arpad_Elo 22 | .. _USCF: http://www.uschess.org/ 23 | 24 | Learning 25 | ~~~~~~~~ 26 | 27 | K-factor and rating extra data 28 | ------------------------------ 29 | 30 | Installing 31 | ~~~~~~~~~~ 32 | 33 | The package is available in `PyPI `_. To 34 | install it in your system, use :command:`easy_install`: 35 | 36 | .. sourcecode:: bash 37 | 38 | $ easy_install elo 39 | 40 | Or check out developement version: 41 | 42 | .. sourcecode:: bash 43 | 44 | $ git clone git://github.com/sublee/elo.git 45 | 46 | API 47 | ~~~ 48 | 49 | .. autoclass:: Elo 50 | :members: 51 | 52 | .. autoclass:: Rating 53 | :members: 54 | 55 | .. autoclass:: CountedRating 56 | :members: 57 | 58 | .. autoclass:: TimedRating 59 | :members: 60 | 61 | Licensing and Author 62 | ~~~~~~~~~~~~~~~~~~~~ 63 | 64 | This project is licensed under BSD_. See LICENSE_ for the details. 65 | 66 | I'm `Heungsub Lee`_, a game developer. I've also owned `the Python TrueSkill 67 | project `_. Any regarding questions or patches are 68 | welcomed. 69 | 70 | .. _BSD: http://en.wikipedia.org/wiki/BSD_licenses 71 | .. _LICENSE: https://github.com/sublee/elo/blob/master/LICENSE 72 | .. _Heungsub Lee: http://subl.ee/ 73 | -------------------------------------------------------------------------------- /elo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | elo 4 | ~~~ 5 | 6 | The Elo rating system. 7 | 8 | :copyright: (c) 2012 by Heungsub Lee 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | from datetime import datetime 12 | import inspect 13 | 14 | 15 | __version__ = '0.1.1' 16 | __all__ = ['Elo', 'Rating', 'CountedRating', 'TimedRating', 'rate', 'adjust', 17 | 'expect', 'rate_1vs1', 'adjust_1vs1', 'quality_1vs1', 'setup', 18 | 'global_env', 'WIN', 'DRAW', 'LOSS', 'K_FACTOR', 'RATING_CLASS', 19 | 'INITIAL', 'BETA'] 20 | 21 | 22 | #: The actual score for win. 23 | WIN = 1. 24 | #: The actual score for draw. 25 | DRAW = 0.5 26 | #: The actual score for loss. 27 | LOSS = 0. 28 | 29 | #: Default K-factor. 30 | K_FACTOR = 10 31 | #: Default rating class. 32 | RATING_CLASS = float 33 | #: Default initial rating. 34 | INITIAL = 1200 35 | #: Default Beta value. 36 | BETA = 200 37 | 38 | 39 | class Rating(object): 40 | 41 | try: 42 | __metaclass__ = __import__('abc').ABCMeta 43 | except ImportError: 44 | # for Python 2.5 45 | pass 46 | 47 | value = None 48 | 49 | def __init__(self, value=None): 50 | if value is None: 51 | value = global_env().initial 52 | self.value = value 53 | 54 | def rated(self, value): 55 | """Creates a :class:`Rating` object for the recalculated rating. 56 | 57 | :param value: the recalculated rating value. 58 | """ 59 | return type(self)(value) 60 | 61 | def __int__(self): 62 | """Type-casting to ``int``.""" 63 | return int(self.value) 64 | 65 | def __long__(self): 66 | """Type-casting to ``long``.""" 67 | return long(self.value) 68 | 69 | def __float__(self): 70 | """Type-casting to ``float``.""" 71 | return float(self.value) 72 | 73 | def __nonzero__(self): 74 | """Type-casting to ``bool``.""" 75 | return bool(int(self)) 76 | 77 | def __eq__(self, other): 78 | return float(self) == float(other) 79 | 80 | def __lt__(self, other): 81 | """Is Rating < number. 82 | 83 | :param other: the operand 84 | :type other: number 85 | """ 86 | return self.value < other 87 | 88 | def __le__(self, other): 89 | """Is Rating <= number. 90 | 91 | :param other: the operand 92 | :type other: number 93 | """ 94 | return self.value <= other 95 | 96 | def __gt__(self, other): 97 | """Is Rating > number. 98 | 99 | :param other: the operand 100 | :type other: number 101 | """ 102 | return self.value > other 103 | 104 | def __ge__(self, other): 105 | """Is Rating >= number. 106 | 107 | :param other: the operand 108 | :type other: number 109 | """ 110 | return self.value >= other 111 | 112 | def __iadd__(self, other): 113 | """Rating += number. 114 | 115 | :param other: the operand 116 | :type other: number 117 | """ 118 | self.value += other 119 | return self 120 | 121 | def __isub__(self, other): 122 | """Rating -= number. 123 | 124 | :param other: the operand 125 | :type other: number 126 | """ 127 | self.value -= other 128 | return self 129 | 130 | def __repr__(self): 131 | c = type(self) 132 | ext_params = inspect.getargspec(c.__init__)[0][2:] 133 | kwargs = ', '.join('%s=%r' % (param, getattr(self, param)) 134 | for param in ext_params) 135 | if kwargs: 136 | kwargs = ', ' + kwargs 137 | args = ('.'.join([c.__module__, c.__name__]), self.value, kwargs) 138 | return '%s(%.3f%s)' % args 139 | 140 | 141 | try: 142 | Rating.register(float) 143 | except AttributeError: 144 | pass 145 | 146 | 147 | class CountedRating(Rating): 148 | """Increases count each rating recalculation.""" 149 | 150 | times = None 151 | 152 | def __init__(self, value=None, times=0): 153 | self.times = times 154 | super(CountedRating, self).__init__(value) 155 | 156 | def rated(self, value): 157 | rated = super(CountedRating, self).rated(value) 158 | rated.times = self.times + 1 159 | return rated 160 | 161 | 162 | class TimedRating(Rating): 163 | """Writes the final rated time.""" 164 | 165 | rated_at = None 166 | 167 | def __init__(self, value=None, rated_at=None): 168 | self.rated_at = rated_at 169 | super(TimedRating, self).__init__(value) 170 | 171 | def rated(self, value): 172 | rated = super(TimedRating, self).rated(value) 173 | rated.rated_at = datetime.utcnow() 174 | return rated 175 | 176 | 177 | class Elo(object): 178 | 179 | def __init__(self, k_factor=K_FACTOR, rating_class=RATING_CLASS, 180 | initial=INITIAL, beta=BETA): 181 | self.k_factor = k_factor 182 | self.rating_class = rating_class 183 | self.initial = initial 184 | self.beta = beta 185 | 186 | def expect(self, rating, other_rating): 187 | """The "E" function in Elo. It calculates the expected score of the 188 | first rating by the second rating. 189 | """ 190 | # http://www.chess-mind.com/en/elo-system 191 | diff = float(other_rating) - float(rating) 192 | f_factor = 2 * self.beta # rating disparity 193 | return 1. / (1 + 10 ** (diff / f_factor)) 194 | 195 | def adjust(self, rating, series): 196 | """Calculates the adjustment value.""" 197 | return sum(score - self.expect(rating, other_rating) 198 | for score, other_rating in series) 199 | 200 | def rate(self, rating, series): 201 | """Calculates new ratings by the game result series.""" 202 | rating = self.ensure_rating(rating) 203 | k = self.k_factor(rating) if callable(self.k_factor) else self.k_factor 204 | new_rating = float(rating) + k * self.adjust(rating, series) 205 | if hasattr(rating, 'rated'): 206 | new_rating = rating.rated(new_rating) 207 | return new_rating 208 | 209 | def adjust_1vs1(self, rating1, rating2, drawn=False): 210 | return self.adjust(rating1, [(DRAW if drawn else WIN, rating2)]) 211 | 212 | def rate_1vs1(self, rating1, rating2, drawn=False): 213 | scores = (DRAW, DRAW) if drawn else (WIN, LOSS) 214 | return (self.rate(rating1, [(scores[0], rating2)]), 215 | self.rate(rating2, [(scores[1], rating1)])) 216 | 217 | def quality_1vs1(self, rating1, rating2): 218 | return 2 * (0.5 - abs(0.5 - self.expect(rating1, rating2))) 219 | 220 | def create_rating(self, value=None, *args, **kwargs): 221 | if value is None: 222 | value = self.initial 223 | return self.rating_class(value, *args, **kwargs) 224 | 225 | def ensure_rating(self, rating): 226 | if isinstance(rating, self.rating_class): 227 | return rating 228 | return self.rating_class(rating) 229 | 230 | def make_as_global(self): 231 | """Registers the environment as the global environment. 232 | 233 | >>> env = Elo(initial=2000) 234 | >>> Rating() 235 | elo.Rating(1200.000) 236 | >>> env.make_as_global() #doctest: +ELLIPSIS 237 | elo.Elo(..., initial=2000.000, ...) 238 | >>> Rating() 239 | elo.Rating(2000.000) 240 | 241 | But if you need just one environment, use :func:`setup` instead. 242 | """ 243 | return setup(env=self) 244 | 245 | def __repr__(self): 246 | c = type(self) 247 | rc = self.rating_class 248 | if callable(self.k_factor): 249 | f = self.k_factor 250 | k_factor = '.'.join([f.__module__, f.__name__]) 251 | else: 252 | k_factor = '%.3f' % self.k_factor 253 | args = ('.'.join([c.__module__, c.__name__]), k_factor, 254 | '.'.join([rc.__module__, rc.__name__]), self.initial, self.beta) 255 | return ('%s(k_factor=%s, rating_class=%s, ' 256 | 'initial=%.3f, beta=%.3f)' % args) 257 | 258 | 259 | def rate(rating, series): 260 | return global_env().rate(rating, series) 261 | 262 | 263 | def adjust(rating, series): 264 | return global_env().adjust(rating, series) 265 | 266 | 267 | def expect(rating, other_rating): 268 | return global_env().expect(rating, other_rating) 269 | 270 | 271 | def rate_1vs1(rating1, rating2, drawn=False): 272 | return global_env().rate_1vs1(rating1, rating2, drawn) 273 | 274 | 275 | def adjust_1vs1(rating1, rating2, drawn=False): 276 | return global_env().adjust_1vs1(rating1, rating2, drawn) 277 | 278 | 279 | def quality_1vs1(rating1, rating2): 280 | return global_env().quality_1vs1(rating1, rating2) 281 | 282 | 283 | def setup(k_factor=K_FACTOR, rating_class=RATING_CLASS, 284 | initial=INITIAL, beta=BETA, env=None): 285 | if env is None: 286 | env = Elo(k_factor, rating_class, initial, beta) 287 | global_env.__elo__ = env 288 | return env 289 | 290 | 291 | def global_env(): 292 | """Gets the global Elo environment.""" 293 | try: 294 | global_env.__elo__ 295 | except AttributeError: 296 | # setup the default environment 297 | setup() 298 | return global_env.__elo__ 299 | -------------------------------------------------------------------------------- /elopopulars.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from elo import CountedRating, Elo 3 | 4 | 5 | __all__ = ['fide30', 'fide25', 'fide', 'uscf'] 6 | 7 | 8 | class FIDERating(CountedRating): 9 | 10 | stable = False 11 | 12 | def __init__(self, value=None, times=0, stable=None): 13 | if stable is not None: 14 | self.stable = stable 15 | if stable: 16 | times = max(30, times) 17 | super(FIDERating, self).__init__(value, times) 18 | if stable is None: 19 | self.stable = self._should_stable() 20 | 21 | def _should_stable(self): 22 | return self.times >= 30 and self.value >= 2400 23 | 24 | def rated(self, value): 25 | rated = super(FIDERating, self).rated(value) 26 | if rated._should_stable(): 27 | rated.stable = True 28 | return rated 29 | 30 | 31 | def make_fide_k_factor(scarce_games, too_low_rating, stabled): 32 | def fide_k_factor(rating): 33 | if rating.times < 30: 34 | return scarce_games 35 | elif rating.stable: 36 | assert rating.times >= 30 37 | return stabled 38 | assert rating < 2400 39 | assert rating.times >= 30 40 | return too_low_rating 41 | return fide_k_factor 42 | 43 | 44 | #: The new FIDE rating regulations which using 30, 15 & 10 K-factor. FIDE is 45 | #: using this `since July 1, 2011 `_. 47 | fide30 = Elo(make_fide_k_factor(30, 15, 10), FIDERating) 48 | 49 | #: The old FIDE rating regulations which using 25, 15 & 10 K-factor. 50 | fide25 = Elo(make_fide_k_factor(25, 15, 10), FIDERating) 51 | 52 | #: The shortcut to :data:`fide30`. 53 | fide = fide30 54 | 55 | #: The USCF rating regulations. The initial rating is 1300 but USCF defined 56 | #: more complex rule. See `the paper `_ of Prof. Mark E. Glickman. 58 | uscf = Elo(lambda r: 32 if r < 2100 else 24 if r < 2400 else 16, initial=1300) 59 | -------------------------------------------------------------------------------- /elotests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | 4 | from almost import Approximate 5 | from pytest import raises 6 | 7 | from elo import * 8 | from elopopulars import * 9 | 10 | 11 | class almost(Approximate): 12 | 13 | def normalize(self, value): 14 | if isinstance(value, Rating) and not isinstance(value, float): 15 | return self.normalize(float(value)) 16 | elif isinstance(value, (list, tuple)): 17 | try: 18 | if isinstance(value[0], Rating): 19 | # flatten transformed ratings 20 | value = tuple(map(float, value)) 21 | except (TypeError, IndexError): 22 | pass 23 | return super(almost, self).normalize(value) 24 | 25 | 26 | def test_rating(): 27 | if type(Rating) is not type: 28 | assert isinstance(1.99, Rating) 29 | assert issubclass(float, Rating) 30 | assert Rating(100) == 100 31 | assert Rating(100) < 200 32 | assert Rating(100) <= 200 33 | assert not (Rating(100) == 200) 34 | assert not (Rating(100) > 200) 35 | assert not (Rating(100) >= 200) 36 | assert Rating(100) == Rating(100) 37 | assert Rating(100) != Rating(200) 38 | 39 | 40 | def test_initial_rating(): 41 | env = Elo(initial=1000) 42 | assert env.create_rating() == 1000 43 | 44 | 45 | def test_custom_rating_class(): 46 | class CustomRating(Rating): pass 47 | assert Elo(CustomRating(1989), rating_class=CustomRating) 48 | elo = Elo(1989, rating_class=CustomRating) 49 | assert isinstance(elo.create_rating(1989), CustomRating) 50 | 51 | 52 | def test_mixed_rating_class(): 53 | class MixedRating(CountedRating, TimedRating): 54 | pass 55 | env = Elo(rating_class=MixedRating) 56 | rating = env.create_rating(1000) 57 | assert hasattr(rating, 'times') 58 | assert hasattr(rating, 'rated_at') 59 | assert rating.times == 0 60 | assert rating.rated_at is None 61 | rating = env.rate(rating, [(WIN, 1000)]) 62 | assert rating.times == 1 63 | assert rating.rated_at is not None 64 | 65 | 66 | def test_fide(): 67 | """Described in `http://en.wikipedia.org/wiki/Elo_rating_system 68 | #Most_accurate_K-factor`_. 69 | """ 70 | # less than 30 games 71 | assert fide25.k_factor(fide25.create_rating(2500, 0)) == 25 72 | assert fide25.k_factor(fide25.create_rating(5000, 10)) == 25 73 | assert fide25.k_factor(fide25.create_rating(1000, 20)) == 25 74 | # 30 or more games but rating below 2400 75 | assert fide25.k_factor(fide25.create_rating(2000, 30)) == 15 76 | assert fide25.k_factor(fide25.create_rating(2399, 31)) == 15 77 | # reaching rating 2400 (stable=True) and thereafter remains or goes below 78 | assert fide25.k_factor(fide25.create_rating(3000, stable=True)) == 10 79 | assert fide25.k_factor(fide25.create_rating(2400, stable=True)) == 10 80 | assert fide25.k_factor(fide25.create_rating(1200, stable=True)) == 10 81 | assert fide25.k_factor(fide25.create_rating(800, stable=True)) == 10 82 | 83 | 84 | def test_uscf(): 85 | """Described in `http://en.wikipedia.org/wiki/Elo_rating_system 86 | #Most_accurate_K-factor`_. 87 | """ 88 | assert uscf.k_factor(2000) == 32 89 | assert uscf.k_factor(2200) == 24 90 | assert uscf.k_factor(3000) == 16 91 | rating = 1613 92 | series = [(LOSS, 1609), (DRAW, 1477), (WIN, 1388), (WIN, 1586), 93 | (LOSS, 1720)] 94 | assert round(uscf.rate(rating, series)) == 1601 95 | 96 | 97 | def test_mcleopold(): 98 | """The example from `https://github.com/McLeopold/PythonSkills/blob/master 99 | /skills/testsuite/test_elo.py`_. 100 | """ 101 | mcleopold = Elo(k_factor=25, initial=1200) 102 | assert almost(mcleopold.quality_1vs1(1200, 1400)) == 0.4805 103 | assert almost(mcleopold.rate_1vs1(1200, 1400), 2) == (1218.99, 1381.01) 104 | 105 | 106 | def test_zookeeper(): 107 | zookeeper = Elo(lambda r: 32 - math.pi / 2 * r.times, CountedRating) 108 | assert zookeeper.k_factor(zookeeper.create_rating(1500, 0)) == 32 109 | 110 | 111 | def test_moserware(): 112 | """The test cases is from `https://github.com/moserware/Skills`_.""" 113 | f25 = fide25 114 | # FideProvisionalEloCalculatorTests 115 | assert almost(f25.rate_1vs1(1200, 1500), 1) == (1221.3, 1478.8) 116 | assert almost(f25.rate_1vs1(1500, 1200), 1) == (1503.8, 1196.3) 117 | assert almost(f25.rate_1vs1(1200, 1500, drawn=True), 1) == (1208.8, 1491.3) 118 | # FideNonProvisionalEloCalculatorTests 119 | _1200 = f25.create_rating(1200, 30) # k = 15 120 | _2500 = f25.create_rating(2500, stable=True) # k = 10 121 | _2600 = f25.create_rating(2600, stable=True) # k = 10 122 | assert almost(f25.rate_1vs1(_1200, _1200)) == (1207.5, 1192.5) 123 | assert almost(f25.rate_1vs1(_1200, _1200, drawn=True)) == (1200, 1200) 124 | assert almost(f25.rate_1vs1(_2600, _2500)) == (2603.6, 2496.4) 125 | assert almost(f25.rate_1vs1(_2500, _2600)) == (2506.4, 2593.6) 126 | assert almost(f25.rate_1vs1(_2600, _2500, drawn=True)) == (2598.6, 2501.4) 127 | 128 | 129 | def test_based_multiplayer_elo_calculator(): 130 | """The expectation is from `http://elo.divergentinformatics.com/`_.""" 131 | elo = Elo(10) 132 | # 1 vs 1 133 | r1 = 1200 134 | r2 = 800 135 | assert almost(elo.expect(r1, r2)) == 0.909091 136 | assert almost(elo.expect(r2, r1)) == 0.090909 137 | assert almost(elo.rate_1vs1(r2, r1)) == (809.091, 1190.909) 138 | # 1 vs 1 vs 1 139 | r1 = 1500 140 | r2 = 1000 141 | r3 = 2000 142 | assert almost(rate(r1, [(WIN, r2), (WIN, r3)])) == 1510.000 143 | assert almost(rate(r2, [(LOSS, r1), (WIN, r3)])) == 1009.436 144 | assert almost(rate(r3, [(LOSS, r1), (LOSS, r2)])) == 1980.564 145 | # 1 vs 1 on FIDE (25, 15 & 10) 146 | r1 = fide25.create_rating(1200, 0) 147 | r2 = fide25.create_rating(800, 0) 148 | assert fide25.k_factor(r1) == 25 149 | assert fide25.k_factor(r2) == 25 150 | assert almost(map(float, fide25.rate_1vs1(r2, r1))) == (822.727, 1177.273) 151 | # stabled 1 vs 1 on FIDE (25, 15 & 10) 152 | r1 = fide25.create_rating(1200, 40) 153 | r2 = fide25.create_rating(800, 40) 154 | assert fide25.k_factor(r1) == 15 155 | assert fide25.k_factor(r2) == 15 156 | assert almost(map(float, fide25.rate_1vs1(r2, r1))) == (813.636, 1186.364) 157 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Elo 4 | ~~~ 5 | 6 | An implementation of the Elo algorithm for Python. Elo is a rating system among 7 | game players and it is used on many chess tournaments to rank. 8 | 9 | .. sourcecode:: pycon 10 | 11 | >>> from elo import rate_1vs1 12 | >>> rate_1vs1(800, 1200) 13 | (809.091, 1190.909) 14 | 15 | Links 16 | ````` 17 | 18 | - `GitHub repository `_ 19 | - `development version 20 | `_ 21 | 22 | See Also 23 | ```````` 24 | 25 | - `Multiplayer Elo Calculator `_ 26 | - `TrueSkill for Python `_ 27 | 28 | """ 29 | from __future__ import with_statement 30 | import re 31 | from setuptools import setup 32 | from setuptools.command.test import test 33 | import sys 34 | 35 | 36 | # detect the current version 37 | with open('elo.py') as f: 38 | version = re.search(r'__version__\s*=\s*\'(.+?)\'', f.read()).group(1) 39 | assert version 40 | 41 | 42 | # use pytest instead 43 | def run_tests(self): 44 | pyc = re.compile(r'\.pyc|\$py\.class') 45 | test_file = pyc.sub('.py', __import__(self.test_suite).__file__) 46 | raise SystemExit(__import__('pytest').main([test_file])) 47 | test.run_tests = run_tests 48 | 49 | 50 | setup( 51 | name='elo', 52 | version=version, 53 | license='BSD', 54 | author='Heungsub Lee', 55 | author_email=re.sub('((sub).)(.*)', r'\2@\1.\3', 'sublee'), 56 | url='http://github.com/sublee/elo', 57 | description='A rating system for chess tournaments', 58 | long_description=__doc__, 59 | platforms='any', 60 | py_modules=['elo'], 61 | classifiers=['Development Status :: 1 - Planning', 62 | 'Intended Audience :: Developers', 63 | 'License :: OSI Approved :: BSD License', 64 | 'Operating System :: OS Independent', 65 | 'Programming Language :: Python', 66 | 'Programming Language :: Python :: 2', 67 | 'Programming Language :: Python :: 2.5', 68 | 'Programming Language :: Python :: 2.6', 69 | 'Programming Language :: Python :: 2.7', 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.1', 72 | 'Programming Language :: Python :: 3.2', 73 | 'Programming Language :: Python :: 3.3', 74 | 'Programming Language :: Python :: Implementation :: CPython', 75 | 'Programming Language :: Python :: Implementation :: PyPy', 76 | 'Topic :: Games/Entertainment'], 77 | test_suite='elotests', 78 | tests_require=['pytest', 'almost'], 79 | use_2to3=(sys.version_info[0] >= 3), 80 | ) 81 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py25, py26, py27, py31, py32, py33, pypy, jython 3 | 4 | [testenv] 5 | commands = {envpython} setup.py test 6 | --------------------------------------------------------------------------------