Skip to content

Commit

Permalink
Merge pull request #133 from Avasam/develop
Browse files Browse the repository at this point in the history
Release 2020-06-01
  • Loading branch information
Avasam authored Jun 1, 2020
2 parents 06d91ac + 354d9f3 commit cb95fae
Show file tree
Hide file tree
Showing 27 changed files with 924 additions and 754 deletions.
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"ms-python.python",
"sonarsource.sonarlint-vscode",
"ms-vscode.vscode-typescript-tslint-plugin",
"davidanson.vscode-markdownlint"
"davidanson.vscode-markdownlint",
"eamodio.gitlens"
]
}
2 changes: 0 additions & 2 deletions Changescripts/2020-04-10-Added-country-code copy.sql

This file was deleted.

2 changes: 2 additions & 0 deletions Changescripts/2020-04-10-Added-country-code.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `player`
ADD COLUMN `country_code` VARCHAR(6) NULL AFTER `name`;
9 changes: 9 additions & 0 deletions Changescripts/2020-05-28-Added-game_values-column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE `game_values` (
`game_id` VARCHAR(8) NOT NULL,
`category_id` VARCHAR(8) NOT NULL,
`platform_id` VARCHAR(8),
`wr_time` INT NOT NULL,
`wr_points` INT NOT NULL,
`mean_time` INT NOT NULL,
`run_id` VARCHAR(8) NOT NULL,
PRIMARY KEY (`game_id`, `category_id`));
2 changes: 2 additions & 0 deletions Changescripts/Remove runs under a minute in game search.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DELETE FROM game_values
WHERE mean_time < 60;
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ The score is calculated by summing up every valid PB of a user according to a fo
- The leaderboard (for the current sub-category) has at least 3 runs
- Is part of a speedrun leaderboard, not a scoreboard
- After step #4, not all runs have the same time
- After step #4, the mean time is not under a minute
- ILs' mean should not be under their fraction of a minute (see step #7)
2. All runs not considered valid (w/o video/image verification or banned user) runs are removed from the leaderboard and can be considered as non-existant from now on.
3. Remove the last 5% of the leaderboard
4. <TODO: 80/20 soft cutoff> The amount of runners in the leaderboard at this point will be reffered to as "population".
4. <TODO: 80th percentile soft cutoff> From this step onward, the amount of runners in the leaderboard will be reffered to as the "population".
5. Generate a logaritmic curve that looks somewhat like below. Where the mean time = 1 and the last run is worth 0
![Curve Example](/assets/images/Curve%20example.jpg)
- 5.1. A signed standart deviation is obtained for all the runs
- 5.2. The deviation is adjusted so that the last run is worth 0 points. By adding the lowest (unsigned) deviation to the signed deviation
- 5.3. The deviation is then normalized so that the mean time is worth 1 point and the last run is still worth 0 points. By dividing the adjusted deviation with the adjusted lowest deviation
- 5.5. Points for a run are equal to: `e^x`
- `x = normalized_deviation * certainty_adjustment`, capped at π
- `certainty_adjustment = 1 - 1 / (population + 1)`
- 5.3. The deviation is then normalized so that the mean time is worth 1 point and the last run is still worth 0 points. By dividing the adjusted deviation with the adjusted lowest deviation. Capped at π.
- 5.5. Points for a run are equal to: `e^(normalized_deviation * certainty_adjustment)`
- `certainty_adjustment = 1 - 1 / (population - 1)`
6. The points for a run are then multiplied by a "length bonus", the decimal point is shifted to the right by 1 and is capped at 999.99
- `length_bonus = 1 + (wr_time / TIME_BONUS_DIVISOR)`. This is to slightly bonify longuer runs which which usually require more time put in the game to achieve a similar level of execution
- `TIME_BONUS_DIVISOR = 3600 * 12`: 12h (1/2 day) for +100%
Expand All @@ -50,7 +51,7 @@ The score is calculated by summing up every valid PB of a user according to a fo
Get yourself a [MySQL server](https://dev.mysql.com/downloads/mysql/) (PythonAnywhere uses version 5.6.40)
Install [Python](https://www.python.org/downloads/) 3.7+
Install PIP (this should come bundled with python 3.4+)
Run this command through the python interpreter (or prepend with `py -m` in a terminal): `pip install flask flask_cors flask_login flask_sqlalchemy sqlalchemy httplib2 simplejson mysql-connector requests pyjwt`
Run this command through the python interpreter (or prepend with `py -m` in a terminal): `pip install flask flask_cors flask_sqlalchemy sqlalchemy httplib2 simplejson mysql-connector requests pyjwt`
Copy `configs.template.py` as `configs.py` and update the file as needed.
If needed, copy `.env.development` as `.env.development.local` and update the file.

Expand Down
22 changes: 16 additions & 6 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime, timedelta
from flask import Blueprint, current_app, jsonify, request
from functools import wraps
from models import map_to_dto, Player, Schedule, TimeSlot
from models import map_to_dto, GameValues, Player, Schedule, TimeSlot
from sqlalchemy import exc
from typing import Any, Dict, List, Optional, Union, Tuple
from user_updater import get_updated_user
Expand Down Expand Up @@ -330,11 +330,12 @@ def __do_update_player(current_user: Player, name_or_id: str):
__currently_updating_from[current_user.user_id] = now
__currently_updating_to[name_or_id] = now

# Actually do the update process
result = get_updated_user(name_or_id)

# Upon update completing, allow the user to update again
__currently_updating_from.pop(current_user.user_id, None)
try:
# Actually do the update process
result = get_updated_user(name_or_id)
finally:
# Upon update completing, allow the user to update again
__currently_updating_from.pop(current_user.user_id, None)

return jsonify(result), 400 if result["state"] == "warning" else 200

Expand Down Expand Up @@ -368,3 +369,12 @@ def delete_friends_current(current_user: Player, id: str):
return f"Successfully removed user ID \"{id}\" from your friends."
else:
return f"User ID \"{id}\" isn't one of your friends."


"""
Game Search context
"""
@api.route('/game-values', methods=('GET',))
@authentication_required
def get_all_game_values(current_user: Player):
return jsonify(map_to_dto(GameValues.query.all()))
140 changes: 7 additions & 133 deletions flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,9 @@
# samuel.06@hotmail.com
##########################################################################
from api import api
from datetime import date
from flask import Flask, send_from_directory, render_template, request, redirect, url_for
from flask_login import LoginManager, logout_user, login_user, current_user
from models import db, Player
from sqlalchemy import exc
from typing import List, Optional, Union
from user_updater import get_updated_user
from utils import get_file, UserUpdaterError, SpeedrunComError
from flask import Flask, send_file, send_from_directory, redirect, url_for
from models import db
import configs
import json
import traceback

# Setup Flask app
app = Flask(__name__, static_folder="assets")
Expand Down Expand Up @@ -63,21 +55,13 @@
db.app = app
db.init_app(app)

# Setup Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)


@login_manager.user_loader
def load_user(user_id: str) -> Union[Player, None]:
# type: (str) -> Union[Player, None]
return Player.get(user_id)


@app.route('/global-scoreboard', defaults={'asset': 'index.html'})
@app.route('/global-scoreboard/<path:asset>', methods=["GET"])
def react_app(asset: str):
return send_from_directory('global-scoreboard/build/', asset)
def global_scoreboard(asset: str):
if asset[:6] == 'static':
return send_from_directory('global-scoreboard/build/', asset)
return send_file('global-scoreboard/build/index.html')


@app.route('/tournament-scheduler', defaults={'asset': 'index.html'})
Expand All @@ -88,117 +72,7 @@ def tournament_scheduler(asset: str):

@app.route('/', methods=["GET", "POST"])
def index():
global result
form_action: str = request.form.get("action")
if request.method == "GET":
friends: List[Player] = []
if current_user.is_authenticated:
friends = [friend.user_id for friend in current_user.get_friends()]
return render_template(
'index.html',
friends=friends,
bypass_update_restrictions=str(
configs.bypass_update_restrictions).lower(),
current_year=str(date.today().year)
)

elif request.method == "POST" and form_action:
friend_id: str = request.form.get("friend-id")
if form_action == "update-user":
if current_user.is_authenticated or configs.bypass_update_restrictions:
try:
result = get_updated_user(request.form.get("name-or-id"))
except UserUpdaterError as exception:
print("\n{}\n{}".format(exception.args[0]["error"], exception.args[0]["details"]))
result = {"state": "danger",
"message": exception.args[0]["details"]}
except Exception:
print("\nError: Unknown\n{}".format(traceback.format_exc()))
result = {"state": "danger",
"message": traceback.format_exc()}
finally:
return json.dumps(result)
else:
return json.dumps({'state': 'warning',
'message': 'You must be logged in to update a user!'})

elif form_action == "unfriend":
if current_user.is_authenticated:
if friend_id:
if current_user.unfriend(friend_id).rowcount > 0:
return json.dumps(
{'state': 'success',
'message': f"Successfully removed user ID \"{friend_id}\" from your friends."})
else:
return json.dumps({'state': 'warning',
'message': f"User ID \"{friend_id}\" isn't one of your friends."})
else:
return json.dumps({'state': 'warning',
'message': 'You must specify a friend ID to remove!'})
else:
return json.dumps({'state': 'warning',
'message': 'You must be logged in to remove friends!'})

elif form_action == "befriend":
if current_user.is_authenticated:
if friend_id:
try:
result = current_user.befriend(friend_id)
except exc.IntegrityError:
return json.dumps({'state': 'warning',
'message': f"User ID \"{friend_id}\" is already one of your friends."})
else:
if result:
return json.dumps({'state': 'success',
'message': f"Successfully added user ID \"{friend_id}\" as a friend."})
else:
return json.dumps({'state': 'warning',
'message': "You can't add yourself as a friend!"})
else:
return json.dumps({'state': 'warning',
'message': 'You must specify a friend ID to add!'})
else:
return json.dumps({'state': 'warning',
'message': 'You must be logged in to add friends!'})

elif form_action == "login":
api_key = request.form.get("api-key")
print("api_key = ", api_key)
if api_key:
try: # Get user from speedrun.com using the API key
data = get_file("https://www.speedrun.com/api/v1/profile", {"X-API-Key": api_key})["data"]
except SpeedrunComError:
print("\nError: Unknown\n{}".format(traceback.format_exc()))
return json.dumps({'state': 'warning',
'message': 'Invalid API key.'})
except Exception:
print("\nError: Unknown\n{}".format(traceback.format_exc()))
return json.dumps({"state": "danger",
"message": traceback.format_exc()})

user_id: Optional[str] = data["id"]
if user_id: # Confirms the API key is valid
user_name: str = data["names"]["international"]
print(f"Logging in '{user_id}' ({user_name})")

loaded_user = load_user(user_id)
if not loaded_user:
loaded_user = Player.create(user_id, user_name)
login_user(loaded_user)
return json.dumps({'state': 'success',
'message': "Successfully logged in. "
"Please refresh the page if it isn't done automatically."})
else:
return json.dumps({'state': 'warning',
'message': 'Invalid API key.'})
else:
return json.dumps({'state': 'warning',
'message': 'You must specify an API key to log in!'})

elif form_action == "logout":
logout_user()
return redirect(url_for('index'))
return redirect(url_for('index'))
return redirect(url_for('global_scoreboard'))


if __name__ == '__main__':
Expand Down
Loading

0 comments on commit cb95fae

Please sign in to comment.