diff --git a/CHANGES b/CHANGES index 57c8cad..8bb49f8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +Version 0.2.2 +------------- + +Under development. + Version 0.2.1 ------------- diff --git a/setup.py b/setup.py index b84cde1..29ff3a2 100644 --- a/setup.py +++ b/setup.py @@ -84,6 +84,6 @@ def run_tests(self): 'Topic :: Games/Entertainment'], install_requires=['distribute'], test_suite='trueskilltests', - tests_require=['pytest', 'almost'], + tests_require=['pytest', 'almost>=0.1.4'], use_2to3=(sys.version_info[0] >= 3), ) diff --git a/trueskill/__init__.py b/trueskill/__init__.py index 66562eb..456f606 100644 --- a/trueskill/__init__.py +++ b/trueskill/__init__.py @@ -42,7 +42,8 @@ def v_win(diff, draw_margin): mean. """ x = diff - draw_margin - return pdf(x) / cdf(x) + denom = cdf(x) + return (pdf(x) / denom) if denom else -x def v_draw(diff, draw_margin): @@ -51,7 +52,7 @@ def v_draw(diff, draw_margin): a, b = draw_margin - abs_diff, -draw_margin - abs_diff denom = cdf(a) - cdf(b) numer = pdf(b) - pdf(a) - return numer / denom * (-1 if diff < 0 else 1) + return (numer / denom) * (-1 if diff < 0 else 1) def w_win(diff, draw_margin): @@ -60,6 +61,8 @@ def w_win(diff, draw_margin): """ x = diff - draw_margin v = v_win(diff, draw_margin) + if v == -x: + return 1. if diff < 0 else 0. return v * (v + x) diff --git a/trueskill/factorgraph.py b/trueskill/factorgraph.py index 69426a0..d32b904 100644 --- a/trueskill/factorgraph.py +++ b/trueskill/factorgraph.py @@ -39,8 +39,10 @@ def set(self, val): return delta def delta(self, other): - return max(abs(self.tau - other.tau), - math.sqrt(abs(self.pi - other.pi))) + pi_delta = abs(self.pi - other.pi) + if pi_delta == inf: + return 0. + return max(abs(self.tau - other.tau), math.sqrt(pi_delta)) def update_message(self, factor, pi=0, tau=0, message=None): message = message or Gaussian(pi=pi, tau=tau) @@ -193,5 +195,9 @@ def up(self): v = self.v_func(*args) w = self.w_func(*args) denom = (1. - w) - pi, tau = div.pi / denom, (div.tau + sqrt_pi * v) / denom - return val.update_value(self, pi, tau) + if denom: + pi, tau = div.pi / denom, (div.tau + sqrt_pi * v) / denom + else: + pi = tau = inf + delta = val.update_value(self, pi, tau) + return delta if denom else 0 diff --git a/trueskillhelpers.py b/trueskillhelpers.py index 6f0a478..a57eb58 100644 --- a/trueskillhelpers.py +++ b/trueskillhelpers.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import with_statement from contextlib import contextmanager +import functools +import inspect import logging @@ -34,3 +37,27 @@ def force_scipycompat(): t.cdf, t.pdf, t.ppf = c.cdf, c.pdf, c.ppf yield t.cdf, t.pdf, t.ppf = cdf, pdf, ppf + + +def with_or_without_scipy(f=None): + if f is None: + def iterate(): + try: + import scipy + except ImportError: + # without + yield False + else: + # with + yield True + # without + with force_scipycompat(): + yield False + return iterate() + @functools.wraps(f) + def wrapped(*args, **kwargs): + for with_scipy in with_or_without_scipy(): + if 'with_scipy' in inspect.getargspec(f)[0]: + kwargs['with_scipy'] = with_scipy + f(*args, **kwargs) + return wrapped diff --git a/trueskilltests.py b/trueskilltests.py index 4bb3b65..e37a423 100644 --- a/trueskilltests.py +++ b/trueskilltests.py @@ -10,6 +10,11 @@ numpy = False from trueskill import * +from trueskillhelpers import with_or_without_scipy + + +inf = float('inf') +nan = float('nan') class almost(Approximate): @@ -190,6 +195,7 @@ def generate_individual(size, env=None): return generate_teams([1] * size, env) +@with_or_without_scipy def test_n_vs_n(): # 1 vs 1 t1, t2 = generate_teams([1, 1]) @@ -213,6 +219,7 @@ def test_n_vs_n(): (22.802, 8.059), (22.802, 8.059), (22.802, 8.059), (22.802, 8.059)] +@with_or_without_scipy def test_1_vs_n(): t1, = generate_teams([1]) # 1 vs 2 @@ -237,6 +244,7 @@ def test_1_vs_n(): (9.418, 7.917), (9.418, 7.917), (9.418, 7.917), (9.418, 7.917)] +@with_or_without_scipy def test_individual(): # 3 players players = generate_individual(3) @@ -271,6 +279,7 @@ def test_individual(): (17.664, 4.433), (15.653, 4.524), (13.190, 4.711), (9.461, 5.276)] +@with_or_without_scipy def test_multiple_teams(): # 2 vs 4 vs 2 t1 = (Rating(40, 4), Rating(45, 3)) @@ -287,6 +296,7 @@ def test_multiple_teams(): assert almost(quality([t1, t2, t3])) == 0.047 +@with_or_without_scipy def test_upset(): # 1 vs 1 t1, t2 = (Rating(),), (Rating(50, 12.5),) @@ -318,6 +328,7 @@ def test_upset(): (31.751, 3.064), (34.051, 2.541), (38.263, 1.849), (44.118, 0.983)] +@with_or_without_scipy def test_partial_play(): t1, t2 = (Rating(),), (Rating(), Rating()) # each results from C# Skills: @@ -340,6 +351,7 @@ def test_partial_play(): assert almost(quality([t1, t2, t3], [(1,), (0.8, 0.9), (1,)])) == 0.0809 +@with_or_without_scipy def test_partial_play_with_weights_dict(): t1, t2 = (Rating(),), (Rating(), Rating()) assert rate([t1, t2], weights={(0, 0): 0.5, (1, 0): 0.5, (1, 1): 0.5}) == \ @@ -353,6 +365,7 @@ def test_partial_play_with_weights_dict(): # reported bugs +@with_or_without_scipy def test_issue3(): """The `issue #3`_, opened by @youknowone. @@ -398,21 +411,16 @@ def test_issue4(): numpy.seterr(**old_settings) +@with_or_without_scipy def test_issue5(): """The `issue #5`_, opened by @warner121. The result of TrueSkill calculator by Microsoft is N(-273.092, 2.683) and - N(-75.830, 2.080), of Moserware's C# Skills is N(NaN, 2.6826) and + N(-75.830, 2.080), of C# Skills by Moserware is N(NaN, 2.6826) and N(NaN, 2.0798). I choose Microsoft's result as an expectation. .. _issue #5: https://github.com/sublee/trueskill/issues/5 """ - from logging import StreamHandler, DEBUG - from trueskillhelpers import factorgraph_logging, force_scipycompat r1, r2 = Rating(-323.263, 2.965), Rating(-48.441, 2.190) - with factorgraph_logging() as logger: - logger.setLevel(DEBUG) - logger.addHandler(StreamHandler(sys.stderr)) - with force_scipycompat(): - assert almost(rate_1vs1(r1, r2)) == \ - [(-273.092, 2.683), (-75.830, 2.080)] + # the result of C# Skills by Moserware + assert almost(rate_1vs1(r1, r2)) == [(nan, 2.683), (nan, 2.080)]