Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Developed implementation for unit tests, quick ratings, and fixed all ratings #5

Merged
merged 16 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
pip install pytest pytest-cov
pytest tests.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
continue-on-error: true
72 changes: 72 additions & 0 deletions src/ratings_calculator/Profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Class to get cfc profile information of the user"""
import json
import requests


class CFCProfile:
"""User id of the user that we are trying to get data for"""

# default constructor for ratings calculator
def __init__(self, user_id: int, request=True) -> None:
self.user_id = user_id
self.profile = self.initialize_profile(request)
return

def initialize_profile(self, request=True) -> dict:
"""
Gets the profile of the user
:param request: boolean indicating whether to use the web to search for this fvalue or not.
:return: json dictionary mapping of the player and its fields
"""
if request:
URL = f"https://server.chess.ca/api/player/v1/{self.user_id}"
page = requests.get(URL)
return page.json()
else:
# open the json file and place the file as the value into the page
filepath = "player_info.json"
f = open(filepath)
data = json.load(f)
return data

def get_profile(self) -> dict:
"""
Gets the profile of the current user
:return: json dictionary mapping of the player and its fields
"""
return self.profile

def get_events_played(self) -> int:
"""
Gets the number of events that this user has participated in
:return: events that this user has played in
"""
numEvents = 0
if self.profile["player"]["events"] == []:
return 0
else:
return len(self.profile["player"]["events"])

def get_last_tournaments(self, num_tournaments: int) -> []:
"""
Gets the number of events that this user has participated in
:param num_tournaments: previous n tournaments to get.
:return: events that this user has played in
"""

tournament_data = []

if num_tournaments > len(self.profile["player"]["events"]):
# if the number of tournaments is greater than the number that exists in the json, take that number
num_tournaments = len(self.profile["player"]["events"])

for i in range(num_tournaments):
tournament_data.append(self.profile["player"]["events"][i])

return tournament_data


class FIDEProfile:
"""Gets user id data for FIDE profile"""

pass
178 changes: 178 additions & 0 deletions src/ratings_calculator/RatingsCalculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import math
from csv import reader


class RatingsCalculator:
"""Ratings Calculator is a class that calculates ratings, cfc-style"""

# default constructor for ratings calculator
def __init__(self) -> None:
self.expected_scores = self.expected_scores_init()

return

def average_rating(self, ratings: list) -> float:
"""
Average ratings of the list of the ratings
:param ratings: a list of ratings
:return: average rating
"""
ave_rating = 0
for rating in ratings:
ave_rating += rating
return ave_rating / len(ratings)

def provisional_unrated_players(self, ratings: list, wins: int, losses: int, games_played: int) -> object:
"""
wins is the number of wins,
losses is the number of losses
games_played is the number of games
ratings is the list of opponent's ratings
"""
Rc = self.average_rating(ratings)
# Rp = Rc + 400 (W - L) / N
Rp = Rc + 400 * (wins - losses) / games_played
return Rp

def established_ratings(self, all_time_high_score: int, old_rating: int, score: float, opponent_ratings: list,
quick=False) -> float:
"""
Returns the established rating from the dictionary of ratings
:param all_time_high_score: the all time high of the player
:param score: the total score from the tournament
:param opponent_ratings: a list of opponent ratings
:param old_rating: the player's old rating
:param quick: if the time control used is between 5 and 14 minutes
:return: returns the established rating of the player
>>> ratingTest = RatingsCalculator()
>>> # expected_dict = ratingTest.expected_scores_init()
>>> ratingTest.established_ratings(1450, 1450, 4, [1237, 1511, 1214, 1441, 1579, 2133])
1487
"""
if score > len(opponent_ratings):
raise Exception("Cannot have higher score than games played")

# K=32 for players under 2200 and K=16 for players at or over 2200;
# Rn = Ro + 32 (S - Sx)
if old_rating >= 2199:
multiplier = 16
else:
multiplier = 32

# if quick (where time control used is between 5-14 min)
if quick:
multiplier = multiplier // 2

# expected_scores_dict = self.expected_scores_init()

expected_total = self.expected_total_score(old_rating, opponent_ratings)
new_rating = old_rating + multiplier * (score - expected_total)

if new_rating > all_time_high_score:
all_time_high_valid = 1
else:
all_time_high_valid = 0

multiplier_e = multiplier // 32 # ratio of the multiplier to ratings under 2200.
bonus = self.bonuses(all_time_high_valid, multiplier_e, new_rating, old_rating, len(opponent_ratings))
sum_of_scores = new_rating + bonus
return sum_of_scores

def bonuses(self, a: int, k_factor_e: int, r_new: float, r_old: int, n: int) -> int:
"""
Returns the total sum of all bonuses available
:param a: is = 1 if all time high, 0 otherwise
:param k_factor_e: ratio of the player's k factor to the k factor used for players under 2200
:param r_new: post-event rating
:param r_old: pre-event rating
:param n: number of games played
:return: value of the new rating
>>> ratingTest = RatingsCalculator()
>>> ratingTest.bonuses(0, 1, 1487, 1450, 6)
1496
"""
if n < 4: # no bonus points awarded if less than 4 games played
return 0

R_MAX_BONUS = 20 # constants set
R_CHANGE_BONUS = 1.75
R_CHANGE_THRESHOLD = 13

bonus1 = a * R_MAX_BONUS * k_factor_e
threshold = R_CHANGE_THRESHOLD * k_factor_e * math.sqrt(n)

if r_new > (r_old + threshold):
b = 1
else:
b = 0

bonus2 = b * R_CHANGE_BONUS * (r_new - r_old - threshold) * k_factor_e
total_bonus = round(bonus1 + bonus2)
return total_bonus

def expected_total_score(self, rating: int, opponent_ratings: list):
"""
Returns the expected total score from the tournament
:param rating: is the player's rating
:param opponent_ratings: is the opponent's rating in a lists
>>> ratingTest = RatingsCalculator()
>>> ratingTest.expected_total_score(1450, [1237, 1511, 1214, 1441, 1579, 2133])
2.84
"""
sum_expected = 0
for opp_rating in opponent_ratings:
if rating >= opp_rating: # your rating is bigger than the opponents
sum_expected += self.find_expected_value(rating - opp_rating, 0)
else:
sum_expected += self.find_expected_value(opp_rating - rating, 1)

return sum_expected

def find_expected_value(self, difference: int, lower_val: int):
"""
Returns the expected value of the individual based on whether it is a lower or higher score.
:param lower_val: whether the number is a lower value
:param difference: is the difference in ratings
:return: expected value based on the expected dictionary
"""
expected_dict = self.expected_scores
expected = expected_dict[int(difference)]
if lower_val == 1:
expected = round(1 - expected, 2)
else:
# keep the older value
pass

return expected

def expected_scores_init(self) -> dict:
"""
Returns the expected score dictionary
:return: dictionary containing expected scores
"""
# file is recommend to be in the following format: starting rank, Name of Player, CFC ID
# information of the event
file_name = "C:\\Users\\zheng\\PycharmProjects\\ratings-calculator\\ExpectedScores.csv"
expected_scores_higher = {}

with open(file_name, 'r') as f:
csv_reader = reader(f)
next(csv_reader)

for row in csv_reader:
# iterate through each row
# print(row)
# if it's the last line
max_diff = 2000
if row[0][0:3] == "735":
for j in range(735, max_diff):
expected_scores_higher[j] = 1
else:
row_vals = row[0].split("-")
starting_val = int(row_vals[0])
end_val = int(row_vals[1])

for j in range(starting_val, end_val + 1):
expected_scores_higher[j] = float(row[1]) # the location of the csv file

return expected_scores_higher
Loading
Loading