Merge branch 'feature/elo_system' of jonas/ultimate_tictactoe_server into master
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -58,3 +58,5 @@ docs/_build/ | ||||
| # PyBuilder | ||||
| target/ | ||||
|  | ||||
| # publishing scripts: | ||||
| push_to_test.sh | ||||
|  | ||||
							
								
								
									
										109
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								README.md
									
									
									
									
									
								
							| @ -6,92 +6,6 @@ communication with the web client is done by a (far from any standard and almost | ||||
|  | ||||
|  | ||||
|  | ||||
| **register as player:** | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "type": "register", | ||||
|     "data": { | ||||
|         "id": "<player_id>", | ||||
|    		"name": "<player_name>" | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| response: | ||||
|  | ||||
| ```JSON | ||||
| { | ||||
|     "type": "register_response", | ||||
|     "data": { | ||||
|         "success": true, | ||||
|         "msg": "<additional info e.g. in case of error>" | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
| **message from server that game started** | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "type": "game_starts", | ||||
|     "data": { | ||||
|         "msg": "...", | ||||
|         "opponent_name": "...", | ||||
|         "is_first_move": true | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| note: `is_first_move` indicates whether the player or it's opponent begins | ||||
|  | ||||
|  | ||||
|  | ||||
| **move** | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "type": "move", | ||||
|     "data": { | ||||
|         "sub_x": "...", | ||||
|         "sub_y": "...", | ||||
|         "x": "...", | ||||
|         "y": "..." | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| response: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "type": "move_response", | ||||
|     "data": { | ||||
|         "success": true, | ||||
|         "msg": "..." | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
| **end game** | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "type": "end_game", | ||||
|     "data": { | ||||
|         "msg": "..." | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| (response?) | ||||
|  | ||||
|  | ||||
|  | ||||
| ## new version: | ||||
|  | ||||
| **json match state:** | ||||
| @ -107,9 +21,10 @@ response: | ||||
|         "y": "..." | ||||
|     } | ||||
|     game_over: <true | false>, | ||||
|     player_won: <null | <player_name>> | ||||
|     current_player: <null | <player_name>> | ||||
|     player_a: "..." | ||||
|     is_draw: <true | false>, | ||||
|     player_won: <null | <player_name>>, | ||||
|     current_player: <null | <player_name>>, | ||||
|     player_a: "...", | ||||
|     player_b: "..." | ||||
| } | ||||
| ``` | ||||
| @ -341,7 +256,21 @@ response: | ||||
| { | ||||
|     "type": "friends_update", | ||||
|     "data": { | ||||
|         "friends": "<list of friends>" | ||||
|         "friends": "<list of friends>", | ||||
|         "elos": "<list of elo values>" | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
| **elo rank update**: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "type": "elo_update", | ||||
|     "data": { | ||||
|         "elo": <elo_value> | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| @ -6,7 +6,7 @@ from user_manager import UserManager | ||||
| from match_manager import MatchManager | ||||
| from match import Match | ||||
|  | ||||
| from tools import debug | ||||
| from tools import debug, elo_p_win, elo_update | ||||
|  | ||||
|  | ||||
| def parse_message(msg: str): | ||||
| @ -133,6 +133,7 @@ class ConnectionHandler(object): | ||||
|                         "msg": "" | ||||
|                     } | ||||
|                 })) | ||||
|                 await self.send_elo(conn) | ||||
|                 await self.send_friends(conn) | ||||
|                 await self._on_match_state_req(conn, None) | ||||
|                 return conn | ||||
| @ -234,6 +235,7 @@ class ConnectionHandler(object): | ||||
|             })) | ||||
|  | ||||
|             if success: | ||||
|                 await self.send_elo(conn) | ||||
|                 await self.send_friends(conn) | ||||
|                 await self._on_match_state_req(conn, None) | ||||
|  | ||||
| @ -355,7 +357,7 @@ class ConnectionHandler(object): | ||||
|                                 ) | ||||
|                             ) | ||||
|                             return | ||||
|                          | ||||
|  | ||||
|                         if (self.match_manager.is_match(conn.user_name, opponent)): | ||||
|                             await conn.websocket.send( | ||||
|                                 json.dumps( | ||||
| @ -444,6 +446,11 @@ class ConnectionHandler(object): | ||||
|                     } | ||||
|                 })) | ||||
|  | ||||
|                 if match.game_over: | ||||
|                     if match.is_draw or (match.player_won is not None): | ||||
|                         # send rank update | ||||
|                         await self.send_elo(conn) | ||||
|  | ||||
|                 other_user = match.player_a_name if conn.user_name == match.player_b_name else match.player_b_name | ||||
|  | ||||
|                 if other_user in self.open_connections_by_user: | ||||
| @ -457,6 +464,9 @@ class ConnectionHandler(object): | ||||
|                     })) | ||||
|                     if match.game_over: | ||||
|                         self.match_manager.delete_match(match.id) | ||||
|                         if match.is_draw or (match.player_won is not None): | ||||
|                             # send rank update | ||||
|                             await self.send_elo(other_conn) | ||||
|  | ||||
|     async def _on_match_close(self, conn, data): | ||||
|         match = None | ||||
| @ -468,6 +478,36 @@ class ConnectionHandler(object): | ||||
|                 if (match is None): | ||||
|                     return | ||||
|  | ||||
|                 if not match.game_over: | ||||
|                     # check whether both player made a move. If so, the match is ranked as lost for the player who aborted the match | ||||
|                     if match.complete_field.__contains__(Match.FIELD_USER_A) and match.complete_field.__contains__(Match.FIELD_USER_B): | ||||
|  | ||||
|                         # update rankings: | ||||
|                         player_lost = conn.user_name | ||||
|                         player_won = match.player_a_name if player_lost == match.player_b_name else match.player_b_name | ||||
|  | ||||
|                         elo_won = self.user_manager.get_elo(player_won) | ||||
|                         elo_lost = self.user_manager.get_elo(player_lost) | ||||
|  | ||||
|                         # calculate elo values: | ||||
|                         p_won = elo_p_win(elo_won, elo_lost) | ||||
|                         p_lost = 1 - p_won | ||||
|  | ||||
|                         new_elo_won = elo_update(elo_won, 1, p_won) | ||||
|                         new_elo_lost = elo_update(elo_lost, 0, p_lost) | ||||
|  | ||||
|                         self.user_manager.update_elo(player_won, new_elo_won) | ||||
|                         self.user_manager.update_elo(player_lost, new_elo_lost) | ||||
|  | ||||
|                         await self.send_elo(conn) | ||||
|  | ||||
|                         if player_won in self.open_connections_by_user: | ||||
|                             other_conn = self.open_connections_by_user[player_won] | ||||
|                             await self.send_elo(other_conn) | ||||
|                          | ||||
|                         debug( | ||||
|                             f"Match {match.id} is aborted by {player_lost} (against {player_won}). Update elo-rankings: {elo_won}->{new_elo_won} and {elo_lost}->{new_elo_lost}") | ||||
|  | ||||
|                 match.game_over = True | ||||
|  | ||||
|                 match_state = match.to_json_state() | ||||
| @ -567,12 +607,23 @@ class ConnectionHandler(object): | ||||
|                 await self.send_friends(conn) | ||||
|  | ||||
|     async def send_friends(self, conn): | ||||
|         friends = list(self.user_manager.get_friends_for_user(conn.user_name)) | ||||
|  | ||||
|         friends, elos = self.user_manager.get_friends_and_elos_for_user( | ||||
|             conn.user_name) | ||||
|  | ||||
|         await conn.send(json.dumps({ | ||||
|             'type': 'friends_update', | ||||
|             'data': { | ||||
|                 'friends': friends | ||||
|                 'friends': friends, | ||||
|                 'elos': elos | ||||
|             } | ||||
|         })) | ||||
|  | ||||
|     async def send_elo(self, conn): | ||||
|         await conn.send(json.dumps({ | ||||
|             'type': 'elo_update', | ||||
|             'data': { | ||||
|                 'elo': self.user_manager.get_elo(conn.user_name) | ||||
|             } | ||||
|         })) | ||||
|  | ||||
|  | ||||
| @ -19,7 +19,7 @@ def create_tables(): | ||||
|         "DROP TABLE IF EXISTS sessions", | ||||
|         "DROP TABLE IF EXISTS users", | ||||
|         "DROP TABLE IF EXISTS friends", | ||||
|         "CREATE TABLE users (name varchar(16) NOT NULL, pw_hash varchar(128) NOT NULL, pw_salt varchar(32) NOT NULL, last_seen datetime NOT NULL, PRIMARY KEY (name)) CHARACTER SET " + settings.db_charset, | ||||
|         "CREATE TABLE users (name varchar(16) NOT NULL, pw_hash varchar(128) NOT NULL, pw_salt varchar(32) NOT NULL, last_seen datetime NOT NULL, elo int NOT NULL, PRIMARY KEY (name)) CHARACTER SET " + settings.db_charset, | ||||
|         "CREATE TABLE matches (id varchar(32) NOT NULL, user_a varchar(16) NOT NULL, user_b varchar(16) NOT NULL, match_state varchar(4096) NOT NULL, active_user varchar(16), last_active datetime NOT NULL, FOREIGN KEY (user_a) REFERENCES users(name), FOREIGN KEY (user_b) REFERENCES users(name), FOREIGN KEY (active_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset, | ||||
|         "CREATE TABLE sessions (id varchar(32) NOT NULL, registered_user varchar(16), temp_user varchar(16), last_seen datetime NOT NULL, PRIMARY KEY (id), FOREIGN KEY(registered_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset, | ||||
|         "CREATE TABLE friends (user varchar(16) NOT NULL, friend varchar(16) NOT NULL, FOREIGN KEY(user) REFERENCES users(name), FOREIGN KEY(friend) REFERENCES users(name)) CHARACTER SET " + settings.db_charset | ||||
|  | ||||
							
								
								
									
										2
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.py
									
									
									
									
									
								
							| @ -16,7 +16,7 @@ from tools import debug | ||||
|  | ||||
| um = UserManager() | ||||
| sm = SessionManager(datetime.timedelta(hours=12)) | ||||
| mm = MatchManager() | ||||
| mm = MatchManager(user_manager=um) | ||||
| ch = ConnectionHandler(sm, um, mm) | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										45
									
								
								match.py
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								match.py
									
									
									
									
									
								
							| @ -29,19 +29,20 @@ def SimpleDecode(jsonDump): | ||||
|     return np.array(json.loads(jsonDump)) | ||||
|  | ||||
|  | ||||
| FIELD_EMPTY = 0 | ||||
| FIELD_USER_A = 1 | ||||
| FIELD_USER_B = 2 | ||||
| FIELD_DRAW = 3 | ||||
|  | ||||
|  | ||||
| class Match(object): | ||||
|  | ||||
|     FIELD_EMPTY = 0 | ||||
|     FIELD_USER_A = 1 | ||||
|     FIELD_USER_B = 2 | ||||
|     FIELD_DRAW = 3 | ||||
|  | ||||
|     def __init__(self, n, match_id, player_a_name, player_b_name, json_state=None): | ||||
|         self.n = n | ||||
|         self.id = match_id | ||||
|         self.complete_field = np.zeros(shape=(n*n, n*n), dtype=int) | ||||
|         self.global_field = np.zeros(shape=(n, n), dtype=int) | ||||
|         self.player_won = None | ||||
|         self.is_draw = False | ||||
|         self.game_over = False | ||||
|         self.last_move = None | ||||
|         self.is_player_a = True | ||||
| @ -59,9 +60,15 @@ class Match(object): | ||||
|         self.game_over = match_obj['game_over'] | ||||
|         self.last_move = match_obj['last_move'] | ||||
|         self.is_player_a = match_obj['active_player'] == self.player_a_name | ||||
|      | ||||
|  | ||||
|         # draw state w.r.t backward compability | ||||
|         self.is_draw = match_obj['is_draw'] if 'is_draw' in match_obj else False | ||||
|  | ||||
|     def to_json_state(self): | ||||
|         match_obj = { | ||||
|         return json.dumps(self.to_dict_state()) | ||||
|      | ||||
|     def to_dict_state(self): | ||||
|         return { | ||||
|             'complete_field': self.complete_field.tolist(), | ||||
|             'global_field': self.global_field.tolist(), | ||||
|             'last_move': self.last_move, | ||||
| @ -69,11 +76,10 @@ class Match(object): | ||||
|             'player_won': self.player_won, | ||||
|             'active_player': self.player_a_name if self.is_player_a else self.player_b_name, | ||||
|             'player_a': self.player_a_name, | ||||
|             'player_b': self.player_b_name | ||||
|             'player_b': self.player_b_name, | ||||
|             'is_draw': self.is_draw | ||||
|         } | ||||
|  | ||||
|         return json.dumps(match_obj) | ||||
|      | ||||
|     def switch_player_names(self): | ||||
|         tmp = self.player_a_name | ||||
|         self.player_a_name = self.player_b_name | ||||
| @ -99,20 +105,20 @@ class Match(object): | ||||
|             last_sub_x = self.last_move['sub_x'] | ||||
|             last_sub_y = self.last_move['sub_y'] | ||||
|  | ||||
|             if sub_x != last_x and self.global_field[last_y, last_x] == FIELD_EMPTY: | ||||
|             if sub_x != last_x and self.global_field[last_y, last_x] == Match.FIELD_EMPTY: | ||||
|                 # user is not allowed to place everywhere! wrong move! | ||||
|                 return False | ||||
|  | ||||
|             if sub_y != last_y and self.global_field[last_y, last_x] == FIELD_EMPTY: | ||||
|             if sub_y != last_y and self.global_field[last_y, last_x] == Match.FIELD_EMPTY: | ||||
|                 return False | ||||
|  | ||||
|         if self.complete_field[sub_y * self.n + y][sub_x * self.n + x] != FIELD_EMPTY: | ||||
|         if self.complete_field[sub_y * self.n + y][sub_x * self.n + x] != Match.FIELD_EMPTY: | ||||
|             return False | ||||
|          | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def is_full(self, field): | ||||
|         return not field.__contains__(FIELD_EMPTY) | ||||
|         return not field.__contains__(Match.FIELD_EMPTY) | ||||
|  | ||||
|     def check_win(self, field, x, y): | ||||
|         is_col = True | ||||
| @ -157,7 +163,7 @@ class Match(object): | ||||
|         abs_x = sub_x * self.n + x | ||||
|         abs_y = sub_y * self.n + y | ||||
|  | ||||
|         player_mark = FIELD_USER_A if self.is_player_a else FIELD_USER_B | ||||
|         player_mark = Match.FIELD_USER_A if self.is_player_a else Match.FIELD_USER_B | ||||
|  | ||||
|         if not self.is_move_valid(sub_x, sub_y, x, y): | ||||
|             debug("invalid move") | ||||
| @ -170,7 +176,7 @@ class Match(object): | ||||
|         self.last_move = {'sub_x': sub_x, 'sub_y': sub_y, 'x': x, 'y': y} | ||||
|  | ||||
|         # check whether this indicates changes in the global field: | ||||
|         if self.global_field[sub_y, sub_x] != FIELD_EMPTY: | ||||
|         if self.global_field[sub_y, sub_x] != Match.FIELD_EMPTY: | ||||
|             debug("field not empty") | ||||
|             return False | ||||
|  | ||||
| @ -184,10 +190,11 @@ class Match(object): | ||||
|                 self.player_won = self.player_a_name if self.is_player_a else self.player_b_name | ||||
|  | ||||
|         elif self.is_full(subgrid): | ||||
|             self.global_field[sub_y, sub_x] = FIELD_DRAW | ||||
|             self.global_field[sub_y, sub_x] = Match.FIELD_DRAW | ||||
|             if self.is_full(self.global_field): | ||||
|                 self.game_over = True | ||||
|                 self.player_won = None | ||||
|                 self.is_draw = True | ||||
|  | ||||
|         self.is_player_a = not self.is_player_a | ||||
|  | ||||
|  | ||||
| @ -1,18 +1,20 @@ | ||||
| from database_connection import DatabaseConnection, SQLInjectionError, get_sql_time | ||||
| from user_manager import UserManager | ||||
| from user import User | ||||
| import datetime | ||||
| import uuid | ||||
| import settings | ||||
| from match import Match | ||||
|  | ||||
| from tools import debug | ||||
| from tools import debug, elo_p_win, elo_update | ||||
|  | ||||
|  | ||||
| class MatchManager(object): | ||||
|     def __init__(self): | ||||
|     def __init__(self, user_manager: UserManager = None): | ||||
|         self.user_manager = user_manager | ||||
|         pass | ||||
|  | ||||
|     def get_match(self, id): | ||||
|     def get_match(self, id: str) -> Match: | ||||
|         query = "SELECT * FROM matches WHERE id=%s" | ||||
|         result = DatabaseConnection.global_single_query(query, (id)) | ||||
|         if len(result) == 0: | ||||
| @ -21,11 +23,11 @@ class MatchManager(object): | ||||
|                       player_b_name=result[0]['user_b'], json_state=result[0]['match_state']) | ||||
|         return match | ||||
|  | ||||
|     def get_matches_for_user(self, user_name): | ||||
|     def get_matches_for_user(self, user_name: str) -> list: | ||||
|         query = "SELECT * FROM matches WHERE user_a=%s OR user_b=%s" | ||||
|         return DatabaseConnection.global_single_query(query, (user_name, user_name)) | ||||
|  | ||||
|     def create_new_match(self, user_a, user_b): | ||||
|     def create_new_match(self, user_a: str, user_b: str) -> Match: | ||||
|         match_id = uuid.uuid4().hex | ||||
|         # check if already existent (but should not be the case) | ||||
|         if len(DatabaseConnection.global_single_query("SELECT id FROM matches WHERE id=%s", (match_id))) > 0: | ||||
| @ -39,13 +41,57 @@ class MatchManager(object): | ||||
|             query, (match_id, user_a, user_b, match.to_json_state(), match.get_current_player(), get_sql_time(now))) | ||||
|         return match | ||||
|  | ||||
|     def update_match(self, match_id, match): | ||||
|     def update_match(self, match_id: str, match: Match) -> None: | ||||
|         now = get_sql_time(datetime.datetime.now()) | ||||
|         query = "UPDATE matches SET match_state=%s, active_user=%s, last_active=%s WHERE id=%s" | ||||
|         DatabaseConnection.global_single_execution( | ||||
|             query, (match.to_json_state(), match.get_current_player(), now, match_id)) | ||||
|  | ||||
|     def apply_move(self, move_data): | ||||
|         # check whether we have to update the elo values (game over triggered by the last move) | ||||
|         if match.game_over: | ||||
|             if match.player_won is not None: | ||||
|  | ||||
|                 player_won = match.player_won | ||||
|                 player_lost = match.player_a_name if player_won == match.player_b_name else match.player_b_name | ||||
|  | ||||
|                 elo_won = self.user_manager.get_elo(player_won) | ||||
|                 elo_lost = self.user_manager.get_elo(player_lost) | ||||
|  | ||||
|                 # calculate elo values: | ||||
|                 p_won = elo_p_win(elo_won, elo_lost) | ||||
|                 p_lost = 1 - p_won | ||||
|  | ||||
|                 new_elo_won = elo_update(elo_won, 1, p_won) | ||||
|                 new_elo_lost = elo_update(elo_lost, 0, p_lost) | ||||
|  | ||||
|                 self.user_manager.update_elo(player_won, new_elo_won) | ||||
|                 self.user_manager.update_elo(player_lost, new_elo_lost) | ||||
|                  | ||||
|                 debug( | ||||
|                     f"Match {match_id} is won by {player_won} over {player_lost}. Update elo-rankings: {elo_won}->{new_elo_won} and {elo_lost}->{new_elo_lost}") | ||||
|  | ||||
|             elif match.is_draw: | ||||
|  | ||||
|                 elo_a = self.user_manager.get_elo(match.player_a_name) | ||||
|                 elo_b = self.user_manager.get_elo(match.player_b_name) | ||||
|  | ||||
|                 p_a_wins = elo_p_win(elo_a, elo_b) | ||||
|                 p_b_wins = 1 - p_a_wins | ||||
|  | ||||
|                 new_elo_a = elo_update(elo_a, 0.5, p_a_wins) | ||||
|                 new_elo_b = elo_update(elo_b, 0.5, p_b_wins) | ||||
|  | ||||
|                 self.user_manager.update_elo(match.player_a_name, new_elo_a) | ||||
|                 self.user_manager.update_elo(match.player_b_name, new_elo_b) | ||||
|  | ||||
|                 debug( | ||||
|                     f"{match_id} between {match.player_a_name} and {match.player_b_name} ended in draw. Update elo-rankings: {elo_a}->{new_elo_a} and {elo_b}->{new_elo_b}") | ||||
|  | ||||
|             else: | ||||
|                 # someone aborted a match. TODO: apply some penalty to the ranking | ||||
|                 pass | ||||
|  | ||||
|     def apply_move(self, move_data: dict) -> Match: | ||||
|         match = self.get_match(move_data['id']) | ||||
|         if match is None: | ||||
|             return None | ||||
| @ -58,10 +104,10 @@ class MatchManager(object): | ||||
|         debug("updated match") | ||||
|         return match | ||||
|  | ||||
|     def is_match(self, player_a, player_b): | ||||
|     def is_match(self, player_a, player_b) -> bool: | ||||
|         query = "SELECT * FROM matches WHERE (user_a=%s AND user_b=%s) OR (user_a=%s AND user_b=%s)" | ||||
|         return len(DatabaseConnection.global_single_query(query, (player_a, player_b, player_b, player_a))) | ||||
|         return len(DatabaseConnection.global_single_query(query, (player_a, player_b, player_b, player_a))) > 0 | ||||
|  | ||||
|     def delete_match(self, match_id): | ||||
|     def delete_match(self, match_id: str) -> None: | ||||
|         query = "DELETE FROM matches WHERE id=%s" | ||||
|         DatabaseConnection.global_single_execution(query, (match_id)) | ||||
|  | ||||
| @ -12,5 +12,8 @@ db_db = None | ||||
|  | ||||
| db_charset = 'utf8mb4' | ||||
|  | ||||
| # field dimension | ||||
| elo_start_value = 1000 | ||||
| elo_default_k = 20 | ||||
|  | ||||
| # field dimension (warning: this constant is not constantly used) | ||||
| n = 3 | ||||
|  | ||||
							
								
								
									
										12
									
								
								tools.py
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								tools.py
									
									
									
									
									
								
							| @ -1,5 +1,15 @@ | ||||
| import datetime | ||||
| from settings import elo_default_k | ||||
|  | ||||
|  | ||||
| def debug(msg): | ||||
| def debug(msg: str) -> None: | ||||
|     print("[" + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "]: " + msg) | ||||
|  | ||||
|  | ||||
| def elo_p_win(player_elo: int, opponent_elo: int) -> float: | ||||
|     return (1 / (1 + 10**((opponent_elo - player_elo)/400))) | ||||
|  | ||||
|  | ||||
| def elo_update(old_elo: int, single_game_result: float, expected_result: float, k: float = elo_default_k) -> int: | ||||
|     new_elo = old_elo + k * (single_game_result - expected_result) | ||||
|     return round(new_elo) | ||||
|  | ||||
| @ -3,6 +3,7 @@ from user import User | ||||
| import datetime | ||||
| import hashlib | ||||
| import uuid | ||||
| from settings import elo_start_value | ||||
|  | ||||
|  | ||||
| class UserManager(object): | ||||
| @ -41,35 +42,68 @@ class UserManager(object): | ||||
|         assert len(self.get_user(user_name)) == 0 | ||||
|         pw_salt = uuid.uuid4().hex | ||||
|         pw_hash = hashlib.sha512(pw.encode() + pw_salt.encode()).hexdigest() | ||||
|         query = "INSERT INTO users (name, pw_hash, pw_salt, last_seen) VALUES ( %s, %s, %s, %s)" | ||||
|         DatabaseConnection.global_single_execution(query, (user_name, pw_hash, pw_salt, datetime.datetime.now())) | ||||
|      | ||||
|         query = "INSERT INTO users (name, pw_hash, pw_salt, last_seen, elo) VALUES ( %s, %s, %s, %s, %s)" | ||||
|         DatabaseConnection.global_single_execution( | ||||
|             query, (user_name, pw_hash, pw_salt, datetime.datetime.now(), elo_start_value)) | ||||
|  | ||||
|     def touch_user(self, user_name): | ||||
|         matches = self.get_user(user_name) | ||||
|         assert len(matches) == 1 | ||||
|         query = "UPDATE users SET last_seen=%s WHERE name=%s" | ||||
|         DatabaseConnection.global_single_execution(query, (datetime.datetime.now(), user_name)) | ||||
|      | ||||
|         DatabaseConnection.global_single_execution( | ||||
|             query, (datetime.datetime.now(), user_name)) | ||||
|  | ||||
|     def update_elo(self, user_name, new_elo): | ||||
|         query = "UPDATE users SET elo=%s WHERE name=%s" | ||||
|         DatabaseConnection.global_single_execution(query, (new_elo, user_name)) | ||||
|  | ||||
|     def get_elo(self, user_name): | ||||
|         query = "SELECT elo FROM users WHERE name=%s" | ||||
|         q = DatabaseConnection.global_single_query(query, (user_name)) | ||||
|         if len(q) > 0: | ||||
|             return q[0]['elo'] | ||||
|  | ||||
|         return None | ||||
|  | ||||
|     def get_average_elo(self): | ||||
|         query = "SELECT AVG(elo) AS average FROM users" | ||||
|         q = DatabaseConnection.global_single_query(query) | ||||
|         if len(q) > 0: | ||||
|             return float(q[0]['average']) | ||||
|  | ||||
|         return None | ||||
|  | ||||
|     def add_friend_to_user(self, user_name, friend_name): | ||||
|         if len(self.get_user(friend_name)) > 0: | ||||
|             query = "INSERT INTO friends (user, friend) VALUES ( %s, %s)" | ||||
|             DatabaseConnection.global_single_execution(query, (user_name, friend_name)) | ||||
|             DatabaseConnection.global_single_execution( | ||||
|                 query, (user_name, friend_name)) | ||||
|             return True | ||||
|         return False | ||||
|      | ||||
|  | ||||
|     def get_friends_for_user(self, user_name): | ||||
|         query = "SELECT friend FROM friends WHERE user=%s" | ||||
|         tmp = DatabaseConnection.global_single_query(query, (user_name)) | ||||
|         friends = set() | ||||
|         for entry in tmp: | ||||
|             friends.add(entry['friend']) | ||||
|         return friends | ||||
|      | ||||
|         return list(friends) | ||||
|  | ||||
|     def get_friends_and_elos_for_user(self, user_name): | ||||
|         query = "SELECT friends.friend AS friend, users.elo AS elo FROM friends JOIN users ON friends.friend=users.name WHERE friends.user=%s" | ||||
|         tmp = DatabaseConnection.global_single_query(query, (user_name)) | ||||
|         friends = [] | ||||
|         elos = [] | ||||
|         for entry in tmp: | ||||
|             friends.append(entry['friend']) | ||||
|             elos.append(entry['elo']) | ||||
|         return friends, elos | ||||
|  | ||||
|     def remove_friend_from_user(self, user_name, friend_name): | ||||
|         query = "DELETE FROM friends WHERE user=%s AND friend=%s" | ||||
|         DatabaseConnection.global_single_execution(query, (user_name, friend_name)) | ||||
|         DatabaseConnection.global_single_execution( | ||||
|             query, (user_name, friend_name)) | ||||
|  | ||||
|     def get_all_users(self): | ||||
|         query = "SELECT name, last_seen FROM users" | ||||
|         return DatabaseConnection.global_single_query(query) | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user