Skip to content

Commit

Permalink
Merge pull request #5 from Hart-House-Chess-Club/develop
Browse files Browse the repository at this point in the history
Developed implementation for unit tests, quick ratings, and fixed all ratings
  • Loading branch information
victor-zheng-codes committed Dec 28, 2023
2 parents 8fbec38 + e0dd1b2 commit e8c036c
Show file tree
Hide file tree
Showing 7 changed files with 2,189 additions and 179 deletions.
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

0 comments on commit e8c036c

Please sign in to comment.