diff --git a/Culinary/CulinaryWords.txt b/CulinaryWords.txt similarity index 100% rename from Culinary/CulinaryWords.txt rename to CulinaryWords.txt diff --git a/Educational/EducationalWords.txt b/Educationalwords.txt similarity index 100% rename from Educational/EducationalWords.txt rename to Educationalwords.txt diff --git a/Fiction/FictionWords.txt b/FictionWords.txt similarity index 100% rename from Fiction/FictionWords.txt rename to FictionWords.txt diff --git a/Miscellaneous/MiscellaneousWords.txt b/Miscellaneouswords.txt similarity index 100% rename from Miscellaneous/MiscellaneousWords.txt rename to Miscellaneouswords.txt diff --git a/Narratives/NarrativesWords.txt b/Narrativewords.txt similarity index 100% rename from Narratives/NarrativesWords.txt rename to Narrativewords.txt diff --git a/Scenic/ScenicWords.txt b/Scenicwords.txt similarity index 100% rename from Scenic/ScenicWords.txt rename to Scenicwords.txt diff --git a/app.py b/app.py index 1661b680b..5c032cc38 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ -#permanent import +# permanent import import os +import sentence_generator import uuid from dotenv import load_dotenv from flask import Flask, jsonify, redirect, render_template, request, url_for, session @@ -8,15 +9,17 @@ from flask_socketio import SocketIO, disconnect, emit from authlib.integrations.flask_client import OAuth from datetime import datetime, timedelta, timezone -from sqlalchemy.orm import validates #for validation of data in tables -from sqlalchemy import Column, or_ #used for reference to tables' column name +from dateutil import parser +from sqlalchemy.orm import validates # for validation of data in tables +from sqlalchemy import Column, or_ # used for reference to tables' column name from player import player import string from text_generator import Text_Generator -#temporary imports, which will be deleted later +# temporary imports, which will be deleted later import random -#STR_MAX_SIZE = 65535 +# STR_MAX_SIZE = 65535 + class App: """ @@ -33,15 +36,15 @@ class App: # Explicitly load env load_dotenv() - _api_key=os.environ.get("API_KEY") + _api_key = os.environ.get("API_KEY") # Configuration of flask app appConf = { - "OAUTH2_CLIENT_ID": os.environ.get("CLIENT_ID"), - "OAUTH2_CLIENT_SECRET": os.environ.get("CLIENT_SECRET"), - "OAUTH2_META_URL": "https://accounts.google.com/.well-known/openid-configuration", - "FLASK_SECRET": os.environ.get("FLASK_SECRET"), - "FLASK_PORT": 5000 + "OAUTH2_CLIENT_ID": os.environ.get("CLIENT_ID"), + "OAUTH2_CLIENT_SECRET": os.environ.get("CLIENT_SECRET"), + "OAUTH2_META_URL": "https://accounts.google.com/.well-known/openid-configuration", + "FLASK_SECRET": os.environ.get("FLASK_SECRET"), + "FLASK_PORT": 5000 } _app.secret_key = appConf.get("FLASK_SECRET") @@ -58,10 +61,9 @@ class App: server_metadata_url=f"{appConf.get('OAUTH2_META_URL')}", ) - socketio = SocketIO(_app) - + socketio = SocketIO(_app) - def run(self,host: str | None = None,port: int | None = None, debug: bool | None = None, load_dotenv: bool = True,**options): + def run(self, host: str | None = None, port: int | None = None, debug: bool | None = None, load_dotenv: bool = True, **options): """ Calls Flask's run function with the specified parameters to run the backend for the web application.\n Preconditions: host is a valid IP address and port is a valid and open port\n @@ -81,8 +83,7 @@ def run(self,host: str | None = None,port: int | None = None, debug: bool | None server. See :func:`werkzeug.serving.run_simple` for more information. """ - self._app.run(host,port,debug,load_dotenv) - + self._app.run(host, port, debug, load_dotenv) @socketio.on('event') def handle_chat_global(json): @@ -90,7 +91,6 @@ def handle_chat_global(json): print('received my event: ' + str(json)) App.socketio.emit('global chat', json) - @_app.route("/login", methods=["GET", "POST"]) def login(): """ @@ -101,11 +101,10 @@ def login(): if request.method == "POST": # Authenticate the user Close Session when done pass - return render_template("login.html", error=error) - - @_app.route("/",methods=["POST","GET"]) + + @_app.route("/", methods=["POST", "GET"]) def home(): """ Handles the requests made to the home page. @@ -113,7 +112,7 @@ def home(): """ return render_template("base.html", userSession=session.get("user")) - @_app.route("/google-signin", methods=["GET","POST"]) + @_app.route("/google-signin", methods=["GET", "POST"]) def google_login(): """ Handles the requests made to the website where users can log in to google @@ -134,7 +133,7 @@ def google_callback(): try: # Obtain the access token from Google OAuth token = App.oauth.ttyper.authorize_access_token() - + # Check if the "id_token" is present in the token if "id_token" in token: # If the "id_token" is present, indicating a successful login @@ -149,34 +148,36 @@ def google_callback(): # Insert user info into the database if doesn"t exists yet if Database.query(uname, "UserInfo") is None: - Database.insert(UserInfo, _username=uname, _password=token["access_token"], _email=uname) + + Database.insert(UserInfo, _username=uname, _password=token["access_token"], _email=uname, _profile_photo=picture) Database.insert(UserData, _username=uname,_email=uname,_accuracy=0,_wins=0,_losses=0,_freq_mistyped_words=0,_total_playing_time=0,_top_wpm=0,_num_races=0,_user_in_game_picture=picture,_last_login_time=datetime.now(timezone.utc)) user_letter_data = { - "_username": uname, - "_email": uname, - **{f"_{letter}": 0 for letter in string.ascii_lowercase}, - "_comma": 0, - "_period": 0, - "_exclamation": 0, - "_question": 0, - "_hyphen": 0, - "_semicolon": 0, - "_single_quote": 0, - "_double_quote": 0, + "_username": uname, + "_email": uname, + **{f"_{letter}": 0 for letter in string.ascii_lowercase}, + "_comma": 0, + "_period": 0, + "_exclamation": 0, + "_question": 0, + "_hyphen": 0, + "_semicolon": 0, + "_single_quote": 0, + "_double_quote": 0, } - Database.insert(UserLetter,**user_letter_data) + Database.insert(UserLetter, **user_letter_data) else: - Database.update(uname,"UserData",_last_login_time=datetime.now(timezone.utc)) + Database.update(uname, "UserData", + _last_login_time=datetime.now(timezone.utc)) else: # Handle the case where access is denied (user cancelled the login) return "Access denied: Google login was canceled or failed." - + # Redirect to the desired page after successful authentication return redirect("/") except Exception as e: # For if user cancels the login return redirect("/login") - + @_app.route("/authentication", methods=["POST"]) def authenticate(): """ @@ -193,9 +194,10 @@ def authenticate(): # Exist if returned value of query is not None user = Database.query(username, "UserInfo") - # Performs validation + # Performs validation if user is not None and user._password == password: - Database.update(user._username,"UserData",_last_login_time=datetime.now(timezone.utc)) + Database.update(user._username, "UserData", + _last_login_time=datetime.now(timezone.utc)) # Gets avatar playerObj = player(username, user._profile_photo) # Stores the Player object in the session @@ -203,14 +205,14 @@ def authenticate(): # Redirects to a desired page when authentication success return redirect("/") else: - # Raises an error for wrong match - raise ValueError("Invalid username or password") + # Raises an error for wrong match + raise ValueError("Invalid username or password") except Exception as e: # Handles errors error = f"{e}" session["error"] = error return redirect("login") - + @_app.route("/signup", methods=["GET", "POST"]) def signup(): """ @@ -235,37 +237,38 @@ def register(): if Database.query(username, "UserInfo"): session["error"] = "Username already used " return redirect("/signup") - if Database.query(email,"UserInfo"): + if Database.query(email, "UserInfo"): session["error"] = "Email already used " return redirect("/signup") # Stores into database avatar = url_for("static", filename="pics/anonymous.png") - Database.insert(UserInfo, _username=username, _email=email, _password=password) - Database.insert(UserData, _username=username,_email=email,_accuracy=0,_wins=0,_losses=0,_freq_mistyped_words=0,_total_playing_time=0,_top_wpm=0,_num_races=0,_last_login_time=datetime.now(timezone.utc)) + Database.insert(UserInfo, _username=username, _email=email, + _password=password, _profile_photo=avatar) + Database.insert(UserData, _username=username, _email=email, _accuracy=0, _wins=0, _losses=0, _freq_mistyped_words=0, + _total_playing_time=0, _top_wpm=0, _num_races=0, _last_login_time=datetime.now(timezone.utc)) user_letter_data = { - "_username": username, - "_email": email, - **{f"_{letter}": 0 for letter in string.ascii_lowercase}, - "_comma": 0, - "_period": 0, - "_exclamation": 0, - "_question": 0, - "_hyphen": 0, - "_semicolon": 0, - "_single_quote": 0, - "_double_quote": 0, + "_username": username, + "_email": email, + **{f"_{letter}": 0 for letter in string.ascii_lowercase}, + "_comma": 0, + "_period": 0, + "_exclamation": 0, + "_question": 0, + "_hyphen": 0, + "_semicolon": 0, + "_single_quote": 0, + "_double_quote": 0, } - Database.insert(UserLetter,**user_letter_data) + Database.insert(UserLetter, **user_letter_data) # Store session - playerObj = player(username, avatar) - + playerObj = player(username, avatar) + # Stores the Player object in the session session["user"] = playerObj.__json__() # Redirects to the result page return redirect("/") - - + @_app.route("/logout", methods=["GET", "POST"]) def logout(): """ @@ -275,21 +278,28 @@ def logout(): # Pop out the user session session.pop("user", None) return redirect("/") - - - @_app.route("/generate_text/",methods=["GET"]) + + @_app.route("/custom-page") + def custom_page(): + return render_template("custompage.html") + + @_app.route("/generate_text/", methods=["GET"]) def generate_text(): """ Sends back text for the requestor to use :param difficulty :param form : Specifies the form of text generated. Values: 'sentences' or 'word_list' + :param amount : Specifies the amount of text to generate. + :param genre : Specifies the genre of the text. Optional. """ - difficulty = request.args.get("difficulty") - if not difficulty: - difficulty="" - return Text_Generator.generate_text(difficulty,request.args.get("form"),request.args.get("amount")) - - @_app.route("/get_avg_txt_len/",methods=["GET"]) + difficulty = request.args.get("difficulty", "") + form = request.args.get("form") + amount = request.args.get("amount") + # Retrieve genre from request, default to None if not provided + genre = request.args.get("genre", None) + return Text_Generator.generate_text(difficulty, form, amount, genre) + + @_app.route("/get_avg_txt_len/", methods=["GET"]) def get_avg_txt_len(): """ Handles requests to get the average length of a word/sentence from a list @@ -298,14 +308,14 @@ def get_avg_txt_len(): """ difficulty = request.args.get("difficulty") if not difficulty: - difficulty="" + difficulty = "" else: - difficulty+="_" + difficulty += "_" return str(Text_Generator.get_avg_txt_len(Text_Generator.get_txt_list(difficulty+request.args.get("form")+".txt"))) def get_test_client(self): return self._app.test_client() - + @_app.route('/user/') def get_user_data(username): userData = Database.query(str(username), "UserData") @@ -314,15 +324,14 @@ def get_user_data(username): else: return jsonify({ "username": userData._username, - "highestWPM" : userData._top_wpm, + "highestWPM": userData._top_wpm, "wins": userData._wins, "losses": userData._losses, - "accuracy" : userData._accuracy, - "frequentMisTypedWord" : userData._freq_mistyped_words, - "totalTime" : userData._total_playing_time, - "frequentMisTypedWord" : userData._freq_mistyped_words + "accuracy": userData._accuracy, + "frequentMisTypedWord": userData._freq_mistyped_words, + "totalTime": userData._total_playing_time, + "frequentMisTypedWord": userData._freq_mistyped_words }) - @_app.route('/leaderboard/top_n_highest_wpm/', methods=['GET']) def get_top_n_highest_wpm_leaderboard(n): @@ -344,13 +353,13 @@ def get_top_n_highest_wpm_leaderboard(n): return jsonify(leaderboard_info) except Exception as e: return jsonify({'error': str(e)}), 500 - - @_app.route("/update_db",methods=["POST"]) + + @_app.route("/update_db", methods=["POST"]) def update_db(): """ Endpoint called to update user stats post-game """ - #TODO: need to secure data transfer and verify origin + # TODO: need to secure data transfer and verify origin if request.is_json: usr_session = session.get("user") if usr_session: @@ -359,19 +368,25 @@ def update_db(): game_data = request.json num_races = int(user_data._num_races) game_wpm = game_data["wpm"] - Database.update(usr,"UserData",_accuracy=(game_data["accuracy"]+float(user_data._accuracy)*num_races)/(num_races+1),_num_races=num_races+1,_total_playing_time=user_data._total_playing_time+game_data["elapsedTime"],_top_wpm=game_wpm if game_wpm>int(user_data._top_wpm) else int(user_data._top_wpm)) - last_user_race = UserRace.query.filter_by(_username=usr).order_by(UserRace._date_played.desc()).first() + Database.update(usr, "UserData", _accuracy=(game_data["accuracy"]+float(user_data._accuracy)*num_races)/(num_races+1), _num_races=num_races+1, + _total_playing_time=user_data._total_playing_time+game_data["elapsedTime"], _top_wpm=game_wpm if game_wpm > int(user_data._top_wpm) else int(user_data._top_wpm)) + last_user_race = UserRace.query.filter_by( + _username=usr).order_by(UserRace._date_played.desc()).first() if last_user_race: - Database.insert(UserRace,_game_num=int(last_user_race._game_num)+1,_username=usr,_email=str(user_data._email),_average_wpm=game_wpm,_selected_mode=game_data["mode"],_time_limit=game_data.get("timeLimit"),_date_played=datetime.fromisoformat(game_data["date"])) + Database.insert(UserRace, _game_num=int(last_user_race._game_num)+1, _username=usr, _email=str(user_data._email), _average_wpm=game_wpm, + _selected_mode=game_data["mode"], _time_limit=game_data.get("timeLimit"), _date_played=parser.parse(game_data["date"])) else: - Database.insert(UserRace,_game_num=1,_username=usr,_email=str(user_data._email),_average_wpm=game_wpm,_selected_mode=game_data["mode"],_time_limit=game_data.get("timeLimit"),_date_played=datetime.fromisoformat(game_data["date"])) - mistyped_chars = game_data.get("mistypedChars") #expect a dict for this {"_char":mistyped_count} + Database.insert(UserRace, _game_num=1, _username=usr, _email=str(user_data._email), _average_wpm=game_wpm, _selected_mode=game_data["mode"], _time_limit=game_data.get( + "timeLimit"), _date_played=parser.parse(game_data["date"])) # messed this up, so adding this line for testing + # expect a dict for this {"_char":mistyped_count} + mistyped_chars = game_data.get("mistypedChars") if mistyped_chars: - user_letter = Database.query(usr,"UserLetter") + user_letter = Database.query(usr, "UserLetter") try: for char, num in mistyped_chars.items(): - #frontend args for the returned json must match those of the db - setattr(user_letter,f"{char}",getattr(user_letter,f"{char}")+num) + # frontend args for the returned json must match those of the db + setattr(user_letter, f"{char}", getattr( + user_letter, f"{char}")+num) App.db.session.commit() except Exception as e: print(e) @@ -379,8 +394,29 @@ def update_db(): return "Not successful" return "Successful" return "Not successful" + + @_app.route("/generate_text/",methods=["GET"]) + def generate_text(): + """ + Sends back text for the requestor to use + :param difficulty + :param form : Specifies the form of text generated. Values: 'sentences' or 'word_list' + """ + difficulty = request.args.get("difficulty") + wpm = request.args.get("wpm") + if wpm: + wpm = int(wpm) + if wpm>=0 and wpm<=45: + difficulty="easy" + elif wpm>=46 and wpm<=80: + difficulty="medium" + else: + difficulty="hard" + return sentence_generator.generate_sentences(difficulty) + if not difficulty: + difficulty="" + return Text_Generator.generate_text(difficulty,request.args.get("form"),request.args.get("amount")) - @_app.route('/raceData/', methods=['GET', 'POST']) def getUserRaceData(username): try: @@ -401,7 +437,6 @@ def getUserRaceData(username): except Exception as e: return jsonify({'error': str(e)}), 500 - class Database: """ A class representing a database connection and operations. @@ -420,8 +455,8 @@ class Database: query(username: str) Queries a user record from the database. """ - - def __init__(self,app:App, **models): + + def __init__(self, app: App, **models): """ Initializes a Database instance with the provided Flask application and model classes. @@ -438,18 +473,19 @@ def __init__(self,app:App, **models): self._app = app self._models = models - #temporary auto populate and inserting method for anyone want to test the database - #which will be deleted later + # temporary auto populate and inserting method for anyone want to test the database + # which will be deleted later @staticmethod def populate_sample_date(num_rows): """ - no need for documentation + Responsible for auto populating """ try: - current_datetime =datetime.now(timezone.utc) + current_datetime = datetime.now(timezone.utc) for i in range(1, num_rows + 1): - sample_google_id = "".join(random.choices(string.ascii_letters + string.digits, k=10)) #set length of id to ten + sample_google_id = "".join(random.choices( + string.ascii_letters + string.digits, k=10)) # set length of id to ten user_info_data = { "_username": f"user{i}", "_password": f"password{i}", @@ -472,21 +508,21 @@ def populate_sample_date(num_rows): user_letter_data = { "_username": f"user{i}", "_email": f"user{i}@gmail.com", - **{f"_{letter}": random.randint(0,100) for letter in string.ascii_lowercase}, - "_comma": random.randint(0,100), - "_period": random.randint(0,100), - "_exclamation": random.randint(0,100), - "_question": random.randint(0,100), - "_hyphen": random.randint(0,100), - "_semicolon": random.randint(0,100), - "_single_quote": random.randint(0,100), - "_double_quote": random.randint(0,100), + **{f"_{letter}": random.randint(0, 100) for letter in string.ascii_lowercase}, + "_comma": random.randint(0, 100), + "_period": random.randint(0, 100), + "_exclamation": random.randint(0, 100), + "_question": random.randint(0, 100), + "_hyphen": random.randint(0, 100), + "_semicolon": random.randint(0, 100), + "_single_quote": random.randint(0, 100), + "_double_quote": random.randint(0, 100), } user_race_data = { "_username": f"user{i}", "_email": f"user{i}@gmail.com", - "_average_wpm": random.randint(40,100), + "_average_wpm": random.randint(40, 100), "_selected_mode": random.choice(["Practice", "Robot Opponent", "MultiPlayer"]), "_time_limit": random.choice([None, 30, 60, 90]), "_date_played": current_datetime - timedelta(days=i) @@ -500,6 +536,78 @@ def populate_sample_date(num_rows): except Exception as e: print(f"Error while populating sample rows: {e}") + #used for generating mock data with unique wpm + used_top_wpm_values = set() + @staticmethod + def generate_unique_wpm(): + """ + Generate a unique random WPM value that has not been used before. + """ + while True: + wpm_value = random.randint(50, 150) # Adjust the range as needed + if wpm_value not in Database.used_wpm_values: + return wpm_value + + #this method is used to populate a specific user's data in all tables + @staticmethod + def populate_sample_data_for_user(user_name:str): + """ + Responsible for auto populating sample data for a specific user + """ + try: + current_datetime = datetime.now(timezone.utc) + + sample_google_id = "".join(random.choices(string.ascii_letters + string.digits, k=10)) # set length of id to ten + user_info_data = { + "_username": f"{user_name}", + "_password": f"password{user_name}", + "_email": f"{user_name}@gmail.com", + "_profile_photo": f'./static/pics/terraria_player.png', + "_google_id": sample_google_id + } + + # Generate unique top WPM value + top_wpm_value = random.randint(0, 110) + while top_wpm_value in Database.used_top_wpm_values: + top_wpm_value = random.randint(0, 110) + + user_data_data = { + "_username": f"{user_name}", + "_email": f"{user_name}@gmail.com", + "_top_wpm": top_wpm_value, + "_accuracy": 20 + (random.randint(0, 100) * 0.5), + "_wins": random.randint(0, 100), + "_losses": random.randint(0, 100), + "_freq_mistyped_words": f"word{user_name}|mistake{user_name}", + "_total_playing_time": random.randint(0, 100), + } + + # Add the generated top WPM value to the set of used values + Database.used_top_wpm_values.add(top_wpm_value) + + user_letter_data = { + "_username": f"{user_name}", + "_email": f"{user_name}@gmail.com", + **{f"_{letter}": random.randint(0, 100) for letter in string.ascii_lowercase}, + "_comma": random.randint(0, 100), + "_period": random.randint(0, 100), + "_exclamation": random.randint(0, 100), + "_question": random.randint(0, 100), + "_hyphen": random.randint(0, 100), + "_semicolon": random.randint(0, 100), + "_single_quote": random.randint(0, 100), + "_double_quote": random.randint(0, 100), + } + + Database.insert(UserInfo, **user_info_data) + Database.insert(UserData, **user_data_data) + Database.insert(UserLetter, **user_letter_data) + + print(f"Sample data added successfully for user{user_name}") + except Exception as e: + print(f"Error while populating sample data: {e}") + + @staticmethod def insert(db_table, **kwargs): """ @@ -514,31 +622,35 @@ def insert(db_table, **kwargs): :precondition: If provided, `wpm`, `accuracy`, `wins`, `losses`, and `freq_mistyped_words` must be of the correct data types and within acceptable ranges. :postcondition: If successful, a new user record is inserted into the database with password hashed. """ - - #check the provided key arguments based on valid column names - #raise ValueError if invalid column names are found - valid_columns = db_table.__table__.columns.keys() #retrieve all columns" name in the table - #this is the required columns that must have a value entered (nullable=False) - required_columns = set(Column.name for Column in db_table.__table__.columns if not Column.nullable) - #invliad columns are the set of argument keys minus the set of valid columns and non-required columns - #this required column is need to find all non-required columns - #this is needed to prevent crash when a valid column is not present in the insert, and viewed as an invliad column - invalid_columns = set(kwargs.keys()) - set(valid_columns) - (set(valid_columns) - required_columns) + + # check the provided key arguments based on valid column names + # raise ValueError if invalid column names are found + # retrieve all columns" name in the table + valid_columns = db_table.__table__.columns.keys() + # this is the required columns that must have a value entered (nullable=False) + required_columns = set( + Column.name for Column in db_table.__table__.columns if not Column.nullable) + # invliad columns are the set of argument keys minus the set of valid columns and non-required columns + # this required column is need to find all non-required columns + # this is needed to prevent crash when a valid column is not present in the insert, and viewed as an invliad column + invalid_columns = set(kwargs.keys()) - set(valid_columns) - \ + (set(valid_columns) - required_columns) if invalid_columns: - raise ValueError(f"Invalid column(s) provided: {','.join(invalid_columns)}") #list of the invalid columns - - #instance of the model with specified column names in parameter + # list of the invalid columns + raise ValueError( + f"Invalid column(s) provided: {','.join(invalid_columns)}") + + # instance of the model with specified column names in parameter new_row = db_table(**kwargs) try: - App.db.session.add(new_row) #add the new row to database table - App.db.session.commit() #commit the transaction/changes + App.db.session.add(new_row) # add the new row to database table + App.db.session.commit() # commit the transaction/changes return new_row except Exception as e: - App.db.session.rollback() #rollback the change made - raise e + App.db.session.rollback() # rollback the change made + raise e - @staticmethod def update(username: str, db_table_name: str, **kwargs): """ @@ -557,60 +669,69 @@ def update(username: str, db_table_name: str, **kwargs): :postcondition: If successful, the user record is updated with the provided values. """ try: - #first validate the table name given in string - valid_table_list = ["UserInfo","UserData","UserLetter", "UserRace"] + # first validate the table name given in string + valid_table_list = ["UserInfo", + "UserData", "UserLetter", "UserRace"] if db_table_name not in valid_table_list: raise ValueError(f"Invalid table name: {db_table_name}") - - #get the table class obj by given table name in string + + # get the table class obj by given table name in string table_obj = globals().get(db_table_name) if table_obj is None: - raise ValueError(f"Table Class Object not found for table name: {db_table_name}") - - #query for user information - user_information = table_obj.query.filter_by(_username=username).first() - if user_information is None: - raise ValueError(f"User '{username}' does not exist in the Database") - + raise ValueError( + f"Table Class Object not found for table name: {db_table_name}") - #after user information is query, perform a check of if user is trying to update their _username - #check if the updating username is unique in the database - new_username = kwargs.get("_username") #get the value based on the key - if new_username and new_username != username: #unique - existing_user = table_obj.query.filter_by(_username=new_username).first() + # query for user information + user_information = table_obj.query.filter_by( + _username=username).first() + if user_information is None: + raise ValueError( + f"User '{username}' does not exist in the Database") + + # after user information is query, perform a check of if user is trying to update their _username + # check if the updating username is unique in the database + # get the value based on the key + new_username = kwargs.get("_username") + if new_username and new_username != username: # unique + existing_user = table_obj.query.filter_by( + _username=new_username).first() if existing_user: - raise ValueError(f"Username '{new_username}' already exists in the Database") - #does the same check for email address + raise ValueError( + f"Username '{new_username}' already exists in the Database") + # does the same check for email address new_email = kwargs.get("_email") if new_email and new_email != user_information._email: - existing_email = table_obj.query.filter_by(_email=new_email).first() + existing_email = table_obj.query.filter_by( + _email=new_email).first() if existing_email: - raise ValueError(f"Email '{new_email}' already exists in the Database") - - #validates and update the provided fields - #key is the column name, value is the updating data + raise ValueError( + f"Email '{new_email}' already exists in the Database") + + # validates and update the provided fields + # key is the column name, value is the updating data for key, value in kwargs.items(): - #ensuring the fields/columns exist in the according table - if hasattr(table_obj, key): #table_obj is referring to the table class object + # ensuring the fields/columns exist in the according table + if hasattr(table_obj, key): # table_obj is referring to the table class object setattr(user_information, key, value) else: - raise AttributeError(f"Attribute '{key}' does not exist in the '{db_table_name}' table") - + raise AttributeError( + f"Attribute '{key}' does not exist in the '{db_table_name}' table") - #if new_username and new_username != username: - #Database.update_username(username, new_username) + # if new_username and new_username != username: + # Database.update_username(username, new_username) - #commit the updated values and fields + # commit the updated values and fields App.db.session.commit() - print(f"User '{username}' record updated successfully in table '{db_table_name}'") + print( + f"User '{username}' record updated successfully in table '{db_table_name}'") except Exception as e: App.db.session.rollback() - print(f"Error in updating user '{username}' in table '{db_table_name}' : {e}") - + print( + f"Error in updating user '{username}' in table '{db_table_name}' : {e}") @staticmethod - def query(identifier: str, db_table_class: str): - #changes made: being able to query by either _username or _email using or_ operator provided by sqlalchemy + def query(identifier: str, db_table_class: str): + # changes made: being able to query by either _username or _email using or_ operator provided by sqlalchemy """ Query a user record from the database using either username or email address. @@ -627,28 +748,31 @@ def query(identifier: str, db_table_class: str): :postcondition: If a user with the provided username/email exists in the database, returns the corresponding User Data object; otherwise, returns None. """ try: - #a list of valid table names - valid_table_list = ["UserInfo","UserData","UserLetter","UserRace"] - #validates if the given string is in the list + # a list of valid table names + valid_table_list = ["UserInfo", + "UserData", "UserLetter", "UserRace"] + # validates if the given string is in the list if db_table_class in valid_table_list: - #find the table class object by the given string + # find the table class object by the given string table_name_obj = globals().get(db_table_class) - #retriving data by sqlalchemy"s query and filter - retrieved_data = table_name_obj.query.filter(or_(table_name_obj._username == identifier, table_name_obj._email == identifier)).first() - #filter_by takes kwargs, not positional arguments - #if user does not exist, return nothing + # retriving data by sqlalchemy"s query and filter + retrieved_data = table_name_obj.query.filter(or_( + table_name_obj._username == identifier, table_name_obj._email == identifier)).first() + # filter_by takes kwargs, not positional arguments + # if user does not exist, return nothing if retrieved_data is None: print(f"Invalid username/email entered: {identifier}") return None - #user information object returned + # user information object returned return retrieved_data else: - raise ValueError(f"Invalid table name: {db_table_class}") #handles invalid table name string + # handles invalid table name string + raise ValueError(f"Invalid table name: {db_table_class}") except Exception as e: - - print(f"Error in querying user information from {db_table_class}: {e}") - return None + print( + f"Error in querying user information from {db_table_class}: {e}") + return None @staticmethod def delete(username: str): @@ -664,26 +788,26 @@ def delete(username: str): :precondition: `username` must be a valid user identifier. :postcondition: If a user with the provided username exists in the database, the corresponding user record is deleted. """ - + try: - #the first index/result filtered by the username + # the first index/result filtered by the username delete_user = UserInfo.query.filter_by(_username=username).first() - #print("the user is: ", delete_user) + # print("the user is: ", delete_user) if delete_user: - #if username exists delete it and return True + # if username exists delete it and return True App.db.session.delete(delete_user) App.db.session.commit() return True - #else username does not exist + # else username does not exist else: return False except Exception as e: - #roll back transaction if error occurred + # roll back transaction if error occurred App.db.session.rollback() return False - - @staticmethod + + @staticmethod def get_top_n_letters(username: str, n: int): """ Return a (sorted in DESC)list of letters according to the Top-N largest corresponding values(mistyped letter times) in UserLetter Table @@ -696,32 +820,34 @@ def get_top_n_letters(username: str, n: int): :return: List containing the Top-N letters in DESC order :rtype: list """ - try: - #validate if user exist in UserInfo + try: + # validate if user exist in UserInfo user_info = UserInfo.query.filter_by(_username=username).first() if not user_info: print(f"User '{username}' does not exist") - return [] - #validate n - max_n = 26 + 8 #eight punctuations added + return [] + # validate n + max_n = 26 + 8 # eight punctuations added if n < 1 or n > max_n: print("Invalid value for 'n', Only 26 Letters and 8 Punctuations") return [] - #query using username the user letter data - user_letter_data = UserLetter.query.filter_by(_username=username).first() - #return empty list if user letter data is None + # query using username the user letter data + user_letter_data = UserLetter.query.filter_by( + _username=username).first() + # return empty list if user letter data is None if not user_letter_data: print(f"No Data Found For User '{username}'") return [] - #a dictionary with letters as keys and mistyped letter times as the number value - #loop through each letter in the alaphbets + # a dictionary with letters as keys and mistyped letter times as the number value + # loop through each letter in the alaphbets letter_number_dict = {} for letter in string.ascii_lowercase: column_name = f"_{letter}" - letter_number_dict[letter] = getattr(user_letter_data, column_name) + letter_number_dict[letter] = getattr( + user_letter_data, column_name) - #added punctuations - #list of added punctuations + # added punctuations + # list of added punctuations punctuation_marks = [",", ".", "!", "?", "-", ";", "'", '"'] punct_names = { ",": "_comma", @@ -737,21 +863,26 @@ def get_top_n_letters(username: str, n: int): for mark in punctuation_marks: get_punct = punct_names.get(mark) if get_punct: - letter_number_dict[mark] = getattr(user_letter_data, get_punct) - - #sort the dict by top-n values in desc order, returning a list - sorted_values = sorted(letter_number_dict, key=letter_number_dict.get, reverse=True)[:n] #n here is not inclusive - #since there is a _ as the first index, it needs to be removed, starting each string with [1:] - #rm_underscore = [letter[1:] for letter in sorted_values] + letter_number_dict[mark] = getattr( + user_letter_data, get_punct) + + # sort the dict by top-n values in desc order, returning a list + sorted_values = sorted(letter_number_dict, key=letter_number_dict.get, reverse=True)[ + :n] # n here is not inclusive + # since there is a _ as the first index, it needs to be removed, starting each string with [1:] + # rm_underscore = [letter[1:] for letter in sorted_values] return sorted_values except Exception as e: - print(f"Error while retrieving top {n} largest values for corresponding letters for user '{username}' : {e}") + print( + f"Error while retrieving top {n} largest values for corresponding letters for user '{username}' : {e}") return [] -#these two tables/classes are not limited to parent/child relationship -#they"re bidirectional, you can retrieve the relative data of the other table by calling either table -#UserData table will have the foreign key -#responsible for storing user"s personal information +# these two tables/classes are not limited to parent/child relationship +# they"re bidirectional, you can retrieve the relative data of the other table by calling either table +# UserData table will have the foreign key +# responsible for storing user"s personal information + + class UserInfo(App.db.Model): """ @@ -763,25 +894,32 @@ class UserInfo(App.db.Model): _registered_date : record of the date and time in UTC when user registered _google_id : identification for third party user(sign in via email) """ - _username =App.db.Column(App.db.String(30), primary_key=True) #primary_key makes username not null and unique - _password =App.db.Column(App.db.String(30)) #password can be null for login with email - _email = App.db.Column(App.db.String(60), unique=True) #this will be kept nullable for now, if required later, this will be changed, along with the other tables + _username = App.db.Column(App.db.String( + 30), primary_key=True) # primary_key makes username not null and unique + # password can be null for login with email + _password = App.db.Column(App.db.String(30)) + # this will be kept nullable for now, if required later, this will be changed, along with the other tables + _email = App.db.Column(App.db.String(60), unique=True) _profile_photo = App.db.Column(App.db.String(255)) - #record the time the user account is created - _registered_date = App.db.Column(App.db.DateTime, default=App.db.func.current_timestamp()) #still in UTC timezone - _google_id = App.db.Column(App.db.String(100)) - - #user_info_ref/user_data_ref are accessors to navigate the relationship between UserData and UserInfo objects - #uselist set to False meaning one-to-one relationship between the two table - #one instance of the user_info is related to one and only one user_data instance (1:1)) - user_data_ref = App.db.relationship("UserData", backref=App.db.backref("user_info_ref_data", uselist=False), cascade="all, delete-orphan", single_parent=True) - #cascade = "all, delete-orphan" when userinfo/data row is deleted, the parent/child row will also be deleted in one-to-one relationship - #since cascade default to be many-to-one relationship(1 userinfo - Many userdata rows), single_parent flag need to set to be True(ensures 1:1) - - #another backref relationship for UserLetter class (for delete) - user_letter_ref = App.db.relationship("UserLetter", backref=App.db.backref("user_info_ref_letter", uselist=False), cascade="all, delete-orphan", single_parent=True) - #backref to UserRace accesssing from UserInfo - user_race_ref = App.db.relationship("UserRace", backref=App.db.backref("user_info_ref_race", uselist=False), cascade="all, delete-orphan", single_parent=True) + # record the time the user account is created + _registered_date = App.db.Column( + App.db.DateTime, default=App.db.func.current_timestamp()) # still in UTC timezone + _google_id = App.db.Column(App.db.String(100)) + + # user_info_ref/user_data_ref are accessors to navigate the relationship between UserData and UserInfo objects + # uselist set to False meaning one-to-one relationship between the two table + # one instance of the user_info is related to one and only one user_data instance (1:1)) + user_data_ref = App.db.relationship("UserData", backref=App.db.backref( + "user_info_ref_data", uselist=False), cascade="all, delete-orphan", single_parent=True) + # cascade = "all, delete-orphan" when userinfo/data row is deleted, the parent/child row will also be deleted in one-to-one relationship + # since cascade default to be many-to-one relationship(1 userinfo - Many userdata rows), single_parent flag need to set to be True(ensures 1:1) + + # another backref relationship for UserLetter class (for delete) + user_letter_ref = App.db.relationship("UserLetter", backref=App.db.backref( + "user_info_ref_letter", uselist=False), cascade="all, delete-orphan", single_parent=True) + # backref to UserRace accesssing from UserInfo + user_race_ref = App.db.relationship("UserRace", backref=App.db.backref( + "user_info_ref_race", uselist=False), cascade="all, delete-orphan", single_parent=True) class UserData(App.db.Model): @@ -799,37 +937,40 @@ class UserData(App.db.Model): _last_login_time : records the last login time of an user _num_races : records the total number of races played by user """ - #_user_data_id = App.db.Column(App.db.Integer, primary_key=True) #should not be manually inserted - _username = App.db.Column(App.db.String(30),App.db.ForeignKey("user_info._username"), primary_key=True) #foreign key referencing UserInfo table + # _user_data_id = App.db.Column(App.db.Integer, primary_key=True) #should not be manually inserted + _username = App.db.Column(App.db.String(30), App.db.ForeignKey( + "user_info._username"), primary_key=True) # foreign key referencing UserInfo table _email = App.db.Column(App.db.String(60), unique=True) - #this "user_info" from the above line is mentioning the table name of UserInfo - #this underscore and the lower case is automated by the system + # this "user_info" from the above line is mentioning the table name of UserInfo + # this underscore and the lower case is automated by the system _accuracy = App.db.Column(App.db.Float) _wins = App.db.Column(App.db.Integer, default=0) _losses = App.db.Column(App.db.Integer, default=0) _freq_mistyped_words = App.db.Column(App.db.Text) _total_playing_time = App.db.Column(App.db.Integer, default=0) - #newly added + # newly added _top_wpm = App.db.Column(App.db.SmallInteger, default=0) - _user_in_game_picture = App.db.Column(App.db.String(100)) #should be different from login profile photo - _last_login_time = App.db.Column(App.db.DateTime) #need configuration later to log user's lastest login time + # should be different from login profile photo + _user_in_game_picture = App.db.Column(App.db.String(100)) + # need configuration later to log user's lastest login time + _last_login_time = App.db.Column(App.db.DateTime) _num_races = App.db.Column(App.db.Integer, default=0) - #validation of whether the username exists in table "user_info" when adding to user_data table - #this ensures data integrity, sqlalchemy will automatically call this method whenever data is trying to be inserted - #when inserting/update a row into user_data - #try/except should be used to catch ValueError exception to avoid crash of system - #mainly used for update/query/delete method, insert cannot be checked by this validation + # validation of whether the username exists in table "user_info" when adding to user_data table + # this ensures data integrity, sqlalchemy will automatically call this method whenever data is trying to be inserted + # when inserting/update a row into user_data + # try/except should be used to catch ValueError exception to avoid crash of system + # mainly used for update/query/delete method, insert cannot be checked by this validation @validates("_username") def validate_username(self, key, _username): try: - #selects the first result filtered using username by sqlalchemy + # selects the first result filtered using username by sqlalchemy user_info = UserInfo.query.filter_by(_username=_username).first() - if user_info is None: # user_info is None if user does not exist + if user_info is None: # user_info is None if user does not exist raise ValueError(f"User '{_username}' does not exist") - except ValueError as e: #handled within the method + except ValueError as e: # handled within the method print(f"Error: {e}") return None return _username @@ -845,8 +986,9 @@ class UserLetter(App.db.Model): _punctuation : punctuations are stored in English words """ - #_user_letter_id = App.db.Column(App.db.Integer, primary_key=True) - _username = App.db.Column(App.db.String(30), App.db.ForeignKey("user_info._username"), primary_key=True) #onupdate="CASCADE" + # _user_letter_id = App.db.Column(App.db.Integer, primary_key=True) + _username = App.db.Column(App.db.String(30), App.db.ForeignKey( + "user_info._username"), primary_key=True) # onupdate="CASCADE" _email = App.db.Column(App.db.String(60), unique=True) _a = App.db.Column(App.db.Integer, default=0) _b = App.db.Column(App.db.Integer, default=0) @@ -874,28 +1016,29 @@ class UserLetter(App.db.Model): _x = App.db.Column(App.db.Integer, default=0) _y = App.db.Column(App.db.Integer, default=0) _z = App.db.Column(App.db.Integer, default=0) - _comma = App.db.Column(App.db.Integer, default=0) # , - _period = App.db.Column(App.db.Integer, default=0) # . - _exclamation = App.db.Column(App.db.Integer, default=0) # ! - _question = App.db.Column(App.db.Integer, default=0) # ? - _hyphen = App.db.Column(App.db.Integer, default=0) # - - _semicolon = App.db.Column(App.db.Integer, default=0) # ; - _single_quote = App.db.Column(App.db.Integer, default=0) # ' - _double_quote = App.db.Column(App.db.Integer, default=0) # " + _comma = App.db.Column(App.db.Integer, default=0) # , + _period = App.db.Column(App.db.Integer, default=0) # . + _exclamation = App.db.Column(App.db.Integer, default=0) # ! + _question = App.db.Column(App.db.Integer, default=0) # ? + _hyphen = App.db.Column(App.db.Integer, default=0) # - + _semicolon = App.db.Column(App.db.Integer, default=0) # ; + _single_quote = App.db.Column(App.db.Integer, default=0) # ' + _double_quote = App.db.Column(App.db.Integer, default=0) # " + # auto checks(by sqlalchemy) whether user exist in the user info table whenever data is inserting/updating - #auto checks(by sqlalchemy) whether user exist in the user info table whenever data is inserting/updating @validates("_username") def validate_username(self, key, _username): try: - user_info_uname = UserInfo.query.filter_by(_username=_username).first() + user_info_uname = UserInfo.query.filter_by( + _username=_username).first() if user_info_uname is None: raise ValueError(f"User '{_username}' does not exist") except ValueError as e: print(f"Error: {e}") return None return _username - + class UserRace(App.db.Model): """ @@ -910,23 +1053,26 @@ class UserRace(App.db.Model): _date_played : the date/time the race/practice is initiated """ _game_num = App.db.Column(App.db.Integer, default=0, primary_key=True) - _username = App.db.Column(App.db.String(30), App.db.ForeignKey("user_info._username"), primary_key=True) - #email is in every table for query purposes + _username = App.db.Column(App.db.String(30), App.db.ForeignKey( + "user_info._username"), primary_key=True) + # email is in every table for query purposes _email = App.db.Column(App.db.String(60)) - #different from highest wpm, this is a record of per game/race - _average_wpm = App.db.Column(App.db.Integer, default=0) #a method might be needed to calculate the averagee wpm for each user - #representing the mode selected by user at that game/race instance + # different from highest wpm, this is a record of per game/race + # a method might be needed to calculate the averagee wpm for each user + _average_wpm = App.db.Column(App.db.Integer, default=0) + # representing the mode selected by user at that game/race instance _selected_mode = App.db.Column(App.db.String(20)) - #optional input when user selected a mode with time limit + # optional input when user selected a mode with time limit _time_limit = App.db.Column(App.db.Float) - #records the date user played that race + # records the date user played that race _date_played = App.db.Column(App.db.DateTime) - #regular check of user info username when entering data in UserRace + # regular check of user info username when entering data in UserRace @validates("_username") def validate_username(self, key, _username): try: - user_info_uname = UserInfo.query.filter_by(_username=_username).first() + user_info_uname = UserInfo.query.filter_by( + _username=_username).first() if user_info_uname is None: raise ValueError(f"User '{_username}' does not exist") except ValueError as e: @@ -935,26 +1081,25 @@ def validate_username(self, key, _username): return _username -if __name__=="__main__": +if __name__ == "__main__": app = App() - - #creates database tables and used for testing purposes(insert/update/query/delete) + # creates database tables and used for testing purposes(insert/update/query/delete) with app._app.app_context(): - #app.db.drop_all() + # app.db.drop_all() app.db.create_all() - #sample insert - #there is limitation and constraints in the Columns - #for example, do not repeat the same number in the num_row as it might have repeated _username and _email (which is suppose to be unique) - #if you want to re-populate with the same num_rows, you must run app.db.dropall() before this method - #after testing, you can repeat the number, but preferrably not to do that - #Database.populate_sample_date(100) + # sample insert + # there is limitation and constraints in the Columns + # for example, do not repeat the same number in the num_row as it might have repeated _username and _email (which is suppose to be unique) + # if you want to re-populate with the same num_rows, you must run app.db.dropall() before this method + # after testing, you can repeat the number, but preferrably not to do that + # Database.populate_sample_date(100) - #this method returns a list represention of top-n largest mistyped letters + # this method returns a list represention of top-n largest mistyped letters # top_n_letters = Database.get_top_n_letters("user35", 6) # print(top_n_letters) - - app.socketio.run(app._app, host="localhost", debug=True) \ No newline at end of file + + app.socketio.run(app._app, host="localhost", debug=True) diff --git a/documentation/src/pages/index.module.css b/documentation/src/pages/index.module.css index 9f71a5da7..4d96badaa 100644 --- a/documentation/src/pages/index.module.css +++ b/documentation/src/pages/index.module.css @@ -21,3 +21,4 @@ align-items: center; justify-content: center; } + diff --git a/instance/ThrillTyper.db b/instance/ThrillTyper.db new file mode 100644 index 000000000..169d76ef2 Binary files /dev/null and b/instance/ThrillTyper.db differ diff --git a/requirements.txt b/requirements.txt index ecfbaa02b..009c0d7af 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/sentence_generator.py b/sentence_generator.py index 4f34c8be6..2b2b80bdb 100644 --- a/sentence_generator.py +++ b/sentence_generator.py @@ -9,16 +9,9 @@ def main(): - sentences = generate_sentences() + sentences = generate_sentences("hard") - #File names for each difficulty level - file_names = ['easy_sentences.txt', 'medium_sentences.txt', 'hard_sentences.txt'] - - #Write sentences to their respective files - for i, difficulty_level in enumerate(sentences): - with open(file_names[i], 'w') as file: - for sentence in difficulty_level: - file.write(sentence + '\n') + print(sentences) def is_valid_characters(sentence): allowed_characters = set(string.ascii_letters + string.punctuation + " ") @@ -40,47 +33,47 @@ def is_similar(new_sentence, existing_sentences, first_three_words_set): return False -def generate_sentences(): - sentences_per_difficulty = 10 - sentences = [[], [], []] - unique_checks = set() #Using a set to track unique sentences - first_three_words_set = set() #Set to track first three words of each sentence +def generate_sentences(difficulty): + difficulty_mapping = {"easy": 1, "medium": 2, "hard": 3} + if difficulty not in difficulty_mapping: + return "Invalid difficulty level" + + difficulty_index = difficulty_mapping[difficulty] - 1 + num_sentences = 10 + sentences = [] + unique_checks = set() + first_three_words_set = set() + + prompt = "Create a sentence that has " + if difficulty_index == 0: + prompt += "short and simple words that have letters that are easy to type in sequence." + elif difficulty_index == 1: + prompt += "of medium length, with moderate vocabulary and one comma that have letters that are moderately difficult to type in sequence." + elif difficulty_index == 2: + prompt += "long and complex, using advanced vocabulary, including commas, semicolons, and at least one capitalized proper noun with words that have letters that are difficult to type in sequence." - for i in range(sentences_per_difficulty): - for difficulty in range(1, 4): - prompt = "Create a sentence that has " - if difficulty == 1: - prompt += "short and simple words that have letters that are easy to type in sequence." - elif difficulty == 2: - prompt += "of medium length, with moderate vocabulary and one comma that have letters that are moderately difficult to type in sequence." - elif difficulty == 3: - prompt += "long and complex, using advanced vocabulary, including commas, semicolons, and at least one capitalized proper noun with words that have letters that are difficult to type in sequence." - - attempts = 0 - while attempts < 5: - try: - response = openai.Completion.create( - engine="gpt-3.5-turbo-instruct", - prompt=prompt, - temperature=1.0, - max_tokens=100, - top_p=1, - frequency_penalty=2.0, - presence_penalty=2.0 - ) - generated_sentence = response.choices[0].text.strip() - - #Check if the sentence is valid based on characters and if it is similar - if is_valid_characters(generated_sentence) and not is_similar(generated_sentence, unique_checks, first_three_words_set): - unique_checks.add(generated_sentence) - sentences[difficulty - 1].append(generated_sentence) - first_three_words_set.add(' '.join(generated_sentence.lower().translate(str.maketrans('', '', string.punctuation)).split()[:3])) - break - except Exception as e: - print(f"An error occurred: {e}") - attempts += 1 + attempts = 0 + while len(sentences) < num_sentences and attempts < 50: + try: + response = openai.Completion.create( + engine="gpt-3.5-turbo-instruct", + prompt=prompt, + temperature=1.0, + max_tokens=100, + top_p=1, + frequency_penalty=2.0, + presence_penalty=2.0 + ) + generated_sentence = response.choices[0].text.strip() + if is_valid_characters(generated_sentence) and not is_similar(generated_sentence, unique_checks, first_three_words_set): + unique_checks.add(generated_sentence) + sentences.append(generated_sentence) + first_three_words_set.add(' '.join(generated_sentence.lower().translate(str.maketrans('', '', string.punctuation)).split()[:3])) + except Exception as e: + print(f"An error occurred: {e}") + attempts += 1 - return sentences + return ' '.join(sentences) if __name__ == "__main__": main() \ No newline at end of file diff --git a/static/js/content/About.js b/static/js/content/About.js index 28f1da6f7..cb4ef3aa0 100644 --- a/static/js/content/About.js +++ b/static/js/content/About.js @@ -53,6 +53,7 @@ function About() {

Overall leaderboard is now available for both guests and authenticated users, showing the top 50 ranked fastest typers recorded on the website.

Multiplayer chat rooms are now available for guest and authenticated users to chat.

Import text mode under single player is now available for users to enter self-determined text content for practice mode.

+

Cosmetics Background Pre-configured cosmetics are now added to the single player game mode.

@@ -72,7 +73,7 @@ function About() {
Profile Photo -

Bochi

+

TU-Wenjie-Gao

Profile Photo diff --git a/static/js/content/Dashboard.js b/static/js/content/Dashboard.js index 4344dd74e..bdec1228e 100644 --- a/static/js/content/Dashboard.js +++ b/static/js/content/Dashboard.js @@ -64,7 +64,10 @@ function Dashboard({ userSession }) { Total Playing Time: {Math.floor(userData.totalTime)} minutes

- Accuracy: {userData.accuracy}% + Highest WPM: {userData.highestWPM} WPM +

+

+ Accuracy: {Number(userData.accuracy).toFixed(2)}%

{/* Specific Data, i.e. WPM and Accuracy */} diff --git a/static/js/content/Leaderboard.js b/static/js/content/Leaderboard.js index 56b71f8cc..8c8f2e203 100644 --- a/static/js/content/Leaderboard.js +++ b/static/js/content/Leaderboard.js @@ -42,7 +42,7 @@ function Leaderboard() { {leaderboardData.map((player, index) => ( - Profile + Profile {player.username} diff --git a/static/js/content/Menu.js b/static/js/content/Menu.js index da84da203..0157d726d 100644 --- a/static/js/content/Menu.js +++ b/static/js/content/Menu.js @@ -1,7 +1,8 @@ + function Menu({}) { const buttonStyle = { - width: '500px', - height: '300px', + width: '100%', // Change button width to be responsive + height: 'auto', // Allow button height to adjust based on content fontSize: '2rem', padding: '20px 15px', border: '2px solid #294E95', @@ -33,7 +34,7 @@ function Menu({}) {

Choose Your Game Mode

-
diff --git a/static/js/reusable/RobotOpponent.js b/static/js/reusable/RobotOpponent.js index a2828b8d0..a72fbb04e 100644 --- a/static/js/reusable/RobotOpponent.js +++ b/static/js/reusable/RobotOpponent.js @@ -324,29 +324,35 @@ function RobotOpponent() { } - + function changeBackground(season) { const body = document.body; switch(season) { case 'spring': body.style.backgroundImage = "url('/static/pics/spring.jpg')"; break; - + case 'winter': body.style.backgroundImage = "url('/static/pics/winter.jpg')"; break; + case 'summer': body.style.backgroundImage = "url('/static/pics/summer.jpg')"; break; + case 'autumn': body.style.backgroundImage = "url('/static/pics/autumn.jpg')"; break; + + case 'original': + body.style.backgroundImage = "none"; + break; + default: body.style.backgroundImage = "none"; } } - React.useEffect(() => { const inputBox = document.getElementById("input-box"); const startBtn = document.getElementById("startBtn"); @@ -392,6 +398,8 @@ function RobotOpponent() { + + diff --git a/static/js/reusable/SinglePlayer.js b/static/js/reusable/SinglePlayer.js index 621799068..95375879d 100644 --- a/static/js/reusable/SinglePlayer.js +++ b/static/js/reusable/SinglePlayer.js @@ -1,118 +1,131 @@ function ThrillTyperGame() { - const date = new Date(); - let text = "Click start button to start!"; - let words = text.split(" "); - var inputGiven = false; - - - let currentCharIndex = 0; //only increment when user has typed correct letter - let currentWordIndex = 0; - let startTime; - //let timerInterval; - let userInputCorrectText = ""; - let correctCharsTyped = 0; // Track correct characters typed - let correctLettersTyped = 0; - let totalCharsTyped = 0; // Track total characters typed - - const intervalRef = React.useRef(null); - - - function startTimerInterval(){ - intervalRef.current = setInterval(updateTimer, 10); - } - - function stopTimerInterval(){ - clearInterval(intervalRef.current); - } - - React.useEffect(() => { - return () => { - clearInterval(intervalRef.current); - }; - }, []); - - async function fetchRandomWordList() { - let newText = ""; - try { - const response = await fetch('/generate_text/?difficulty=easy&form=words&amount=30'); - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - newText = await response.text(); - } catch (error) { - console.error('There was a problem with the fetch operation:', error); - } - return newText; + const date = new Date(); + let text = "Click start button to start!"; + let words = text.split(" "); + + let currentCharIndex = 0; //only increment when user has typed correct letter + let currentWordIndex = 0; + let startTime; + //let timerInterval; + let userInputCorrectText = ""; + let correctCharsTyped = 0; // Track correct characters typed + let totalCharsTyped = 0; // Track total characters typed + + const intervalRef = React.useRef(null); + + function startTimerInterval() { + intervalRef.current = setInterval(updateTimer, 10); + } + + function stopTimerInterval() { + clearInterval(intervalRef.current); + } + + React.useEffect(() => { + return () => { + clearInterval(intervalRef.current); + }; + }, []); + + async function fetchRandomWordList(genre) { + let newText = ""; + try { + // Build the URL based on whether genre is provided + const url = genre + ? `/generate_text/?form=words&amount=30&genre=${genre}` + : "/generate_text/?difficulty=easy&form=words&amount=30"; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + newText = await response.text(); + } catch (error) { + console.error("There was a problem with the fetch operation:", error); } - - async function postUserMetrics(wpm, accuracy, elapsedTime){ - try{ - const postData = {"wpm":wpm,"accuracy":accuracy,"mode":"Single Player","elapsedTime":elapsedTime/60,"date":date.toISOString()} - const response = await fetch('/update_db',{ - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(postData)}); - if(!response.ok){ - throw new Error('Network response was not ok'); - } - } - catch(error){ - console.error('There was a problem with the fetch operation:', error); - } + return newText; + } + + async function postUserMetrics(wpm, accuracy, elapsedTime) { + try { + const postData = { + wpm: wpm, + accuracy: accuracy, + mode: "Single Player", + elapsedTime: elapsedTime / 60, + date: date.toISOString(), + }; + const response = await fetch("/update_db", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(postData), + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + } catch (error) { + console.error("There was a problem with the fetch operation:", error); } + } - //update text color as user types text - //green text if user typed correctly - //red background text if user typed incorrectly - /* + //update text color as user types text + //green text if user typed correctly + //red background text if user typed incorrectly + /* The whole function logic is based on following assumption, we divide text into 3 sections correct text | incorrect text | untyped text */ - //if you don't get what is going on here, open a type racer game and type some wrong text - function updateText() { - var str = text; - var userInputFullText = userInputCorrectText + document.getElementById("input-box").value; - - var greenText = ""; //correct text - var redText = ""; //incorrect text - var uncoloredText = ""; //untyped text - - //green text - //start index is fixed to 0 - //end index is number of matched letters, until the first incorrect letter - var greenStartIndex = 0; - var greenEndIndex = 0; - var numMatchLetters = 0; - for (var i = 0; i < userInputFullText.length; i++) { - if (userInputFullText[i] == text[i]) { //what if userInputFullText is longer than text? could not happend because submission - numMatchLetters++; - } else { - break; - } - } - - greenEndIndex = numMatchLetters; - greenText = text.substring(greenStartIndex, greenEndIndex); + //if you don't get what is going on here, open a type racer game and type some wrong text + function updateText() { + var str = text; + var userInputFullText = + userInputCorrectText + document.getElementById("input-box").value; + + var greenText = ""; //correct text + var redText = ""; //incorrect text + var uncoloredText = ""; //untyped text + + //green text + //start index is fixed to 0 + //end index is number of matched letters, until the first incorrect letter + var greenStartIndex = 0; + var greenEndIndex = 0; + var numMatchLetters = 0; + for (var i = 0; i < userInputFullText.length; i++) { + if (userInputFullText[i] == text[i]) { + //what if userInputFullText is longer than text? could not happend because submission + numMatchLetters++; + } else { + break; + } + } - //red text - //start index is the first unmatched letter, if it exists. It equals to greenEndIndex - //end index is the last index of user input text - var redStartIndex = greenEndIndex; - var redEndIndex = greenEndIndex; - if (numMatchLetters < userInputFullText.length) { //if number of matched letters less than input letters means there are wrong input letters - redEndIndex = userInputFullText.length > text.length ? text.length : userInputFullText.length; //in case user input text is longer than text - } + greenEndIndex = numMatchLetters; + greenText = text.substring(greenStartIndex, greenEndIndex); + + //red text + //start index is the first unmatched letter, if it exists. It equals to greenEndIndex + //end index is the last index of user input text + var redStartIndex = greenEndIndex; + var redEndIndex = greenEndIndex; + if (numMatchLetters < userInputFullText.length) { + //if number of matched letters less than input letters means there are wrong input letters + redEndIndex = + userInputFullText.length > text.length + ? text.length + : userInputFullText.length; //in case user input text is longer than text + } - redText = text.substring(redStartIndex, redEndIndex); + redText = text.substring(redStartIndex, redEndIndex); - //uncoloredText is the rest of the text starting from redEndIndex - uncoloredText = str.substring(redEndIndex); + //uncoloredText is the rest of the text starting from redEndIndex + uncoloredText = str.substring(redEndIndex); - /* debug + /* debug console.log("updateText debugging"); console.log("userInputFullText: " + userInputFullText); console.log("greenText: " + greenText); @@ -120,12 +133,80 @@ function ThrillTyperGame() { console.log("uncoloredText: " + uncoloredText + "\n"); */ - var updatedText = `${greenText}` + - `${redText}` + uncoloredText; - - document.getElementById("text-display").innerHTML = updatedText; - } - + var updatedText = + `${greenText}` + + `${redText}` + + uncoloredText; + + document.getElementById("text-display").innerHTML = updatedText; + } + + async function startTimer(genre = null) { + // Default genre to null if not passed + currentWordIndex = 0; // Initializes value for play again + currentCharIndex = 0; + userInputCorrectText = ""; + document.getElementById("input-box").value = ""; + document.getElementById("result").innerHTML = ""; + + startTime = new Date().getTime(); + text = await fetchRandomWordList(genre); // Call with genre, which could be null + + words = text.split(" "); + + displayText(); + enableInput(); + + stopTimerInterval(); + startTimerInterval(); + } + + function updateTimer() { + const currentTime = new Date().getTime(); + const elapsedTime = (currentTime - startTime) / 1000; + document.getElementById( + "result" + ).innerHTML = `Time elapsed: ${elapsedTime.toFixed(2)} seconds`; + } + + function displayText() { + document.getElementById("text-display").innerHTML = text; + } + + function enableInput() { + document.getElementById("input-box").disabled = false; + document.getElementById("input-box").focus(); + } + + function checkInput() { + var userInputText = document.getElementById("input-box").value; + var userInputLastChar = userInputText[userInputText.length - 1]; + + //updates text color + updateText(); + + //idk what this is + //if typed word matches with text word and last letter is space, clear input box and add word to userInputCorrectText + if (userInputText.substring(0, userInputText.length - 1) == words[currentWordIndex] && userInputLastChar == ' ') { + currentWordIndex++; + userInputCorrectText += userInputText; //saves correct text + document.getElementById("input-box").value = ""; + } + + if (userInputLastChar == text[currentCharIndex]) { //works but logic is bad + currentCharIndex++; + correctCharsTyped++; + if(text[currentCharIndex]!=' '){ + correctLettersTyped++; + } + } + totalCharsTyped++; + + //submit input if last letter is typed + if (currentCharIndex >= text.length) { + submitInput(); + } + } async function startTimer() { currentWordIndex = 0; //initializes value for play again @@ -153,51 +234,54 @@ function ThrillTyperGame() { startTimerInterval(); } - function updateTimer() { - const currentTime = new Date().getTime(); - const elapsedTime = (currentTime - startTime) / 1000; - document.getElementById("result").innerHTML = `Time elapsed: ${elapsedTime.toFixed(2)} seconds`; - } - - function displayText() { - document.getElementById("text-display").innerHTML = text; + function stopTimer() { + stopTimerInterval(); + document.getElementById("input-box").disabled = false; + document.getElementById("input-box").value = ""; + userInputCorrectText = ""; + currentCharIndex = 0; + document.getElementById("result").innerHTML = ""; + currentWordIndex = 0; //initializes value for play again + updateText(); + document.getElementById("text-display").innerHTML = + "Click start button to start!"; + } + + function fillText() { + userInputCorrectText = text.substring(0, text.length - 1); + currentCharIndex = text.length - 1; + updateText(); + //console.log("text: " + text); + //console.log("userInputCorrectText: " + userInputCorrectText); + } + + function changeBackground(season) { + const body = document.body; + switch (season) { + case "spring": + body.style.backgroundImage = "url('/static/pics/spring.jpg')"; + break; + + case "winter": + body.style.backgroundImage = "url('/static/pics/winter.jpg')"; + break; + case "summer": + body.style.backgroundImage = "url('/static/pics/summer.jpg')"; + break; + case "autumn": + body.style.backgroundImage = "url('/static/pics/autumn.jpg')"; + break; + default: + body.style.backgroundImage = "none"; } + } - - function enableInput() { - document.getElementById("input-box").disabled = false; - document.getElementById("input-box").focus(); - } - - function checkInput() { - var userInputText = document.getElementById("input-box").value; - var userInputLastChar = userInputText[userInputText.length - 1]; - - //updates text color - updateText(); - - //idk what this is - //if typed word matches with text word and last letter is space, clear input box and add word to userInputCorrectText - if (userInputText.substring(0, userInputText.length - 1) == words[currentWordIndex] && userInputLastChar == ' ') { - currentWordIndex++; - userInputCorrectText += userInputText; //saves correct text - document.getElementById("input-box").value = ""; - } - - if (userInputLastChar == text[currentCharIndex]) { //works but logic is bad - currentCharIndex++; - correctCharsTyped++; - if(text[currentCharIndex]!=' '){ - correctLettersTyped++; - } - } - totalCharsTyped++; - - //submit input if last letter is typed - if (currentCharIndex >= text.length) { - submitInput(); - } - } + var percentage = 10; + function updateProgressBar() { + percentage += 10; + document.getElementById("hello").style.width = percentage + "%"; + document.getElementById("hello2").innerHTML = percentage + "%"; + } function submitInput() { //clearInterval(timerInterval); @@ -214,19 +298,6 @@ function ThrillTyperGame() { postUserMetrics(wordsPerMinute,accuracy,elapsedTime); } - function stopTimer(){ - stopTimerInterval(); - document.getElementById("input-box").disabled = false; - document.getElementById("input-box").value = ""; - userInputCorrectText = ""; - currentCharIndex = 0; - document.getElementById("result").innerHTML = ""; - currentWordIndex = 0; //initializes value for play again - updateText(); - document.getElementById("text-display").innerHTML = "Click start button to start!"; - inputGiven=false; - } - async function fetchUserInput() { let newText = ""; try { @@ -252,72 +323,57 @@ function ThrillTyperGame() { //console.log("userInputCorrectText: " + userInputCorrectText); } + function insertPlayerStatus() { + document.getElementById("holder").appendChild(makePlayerStatus()); + document.getElementById("holder").appendChild(makePlayerStatus()); + } - - function changeBackground(season) { - const body = document.body; - switch(season) { - case 'spring': - body.style.backgroundImage = "url('/static/pics/spring.jpg')"; - break; - - case 'winter': - body.style.backgroundImage = "url('/static/pics/winter.jpg')"; - break; - case 'summer': - body.style.backgroundImage = "url('/static/pics/summer.jpg')"; - break; - case 'autumn': - body.style.backgroundImage = "url('/static/pics/autumn.jpg')"; - break; - default: - body.style.backgroundImage = "none"; - } - } - - - - var percentage = 10; - function updateProgressBar(){ - percentage += 10; - document.getElementById("hello").style.width = percentage + "%"; - document.getElementById("hello2").innerHTML = percentage + "%"; - } - - function insertPlayerStatus(){ - document.getElementById("holder").appendChild(makePlayerStatus()); - document.getElementById("holder").appendChild(makePlayerStatus()); - } -return ( + return (
-

Thrill Typer Game

-
{text}
- - -
-
-
- - - - - - {/* Dropdown menu */} -
- -
- - - - -
-
- - - +

Thrill Typer Game

+
{text}
+ + +
+
+
+ + + + +
+ +
+ + + + + +
-
-); - -} \ No newline at end of file + {/* New custom button */} + {/* Dropdown menu */} +
+ +
+ + + + +
+
+ + +
+
+ ); +} diff --git a/static/pics/autumn.jpg b/static/pics/autumn.jpg index 834bb7959..1bc144d93 100644 Binary files a/static/pics/autumn.jpg and b/static/pics/autumn.jpg differ diff --git a/static/pics/leaderboard_second.png b/static/pics/leaderboard_second.png index 3ac96a836..1dae2e9c5 100644 Binary files a/static/pics/leaderboard_second.png and b/static/pics/leaderboard_second.png differ diff --git a/static/pics/leaderboard_third.png b/static/pics/leaderboard_third.png index f238ed144..c111b1a4b 100644 Binary files a/static/pics/leaderboard_third.png and b/static/pics/leaderboard_third.png differ diff --git a/static/pics/leaderboard_winner.png b/static/pics/leaderboard_winner.png index 333cdee16..db8f82b5e 100644 Binary files a/static/pics/leaderboard_winner.png and b/static/pics/leaderboard_winner.png differ diff --git a/static/pics/profile_photos/icons8-ahri-100.png b/static/pics/profile_photos/icons8-ahri-100.png new file mode 100644 index 000000000..36e5117d6 Binary files /dev/null and b/static/pics/profile_photos/icons8-ahri-100.png differ diff --git a/static/pics/profile_photos/icons8-animal-100.png b/static/pics/profile_photos/icons8-animal-100.png new file mode 100644 index 000000000..c07df0122 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-100.png differ diff --git a/static/pics/profile_photos/icons8-animal-1001.png b/static/pics/profile_photos/icons8-animal-1001.png new file mode 100644 index 000000000..76148f27d Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1001.png differ diff --git a/static/pics/profile_photos/icons8-animal-10010.png b/static/pics/profile_photos/icons8-animal-10010.png new file mode 100644 index 000000000..e19fd008b Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10010.png differ diff --git a/static/pics/profile_photos/icons8-animal-10011.png b/static/pics/profile_photos/icons8-animal-10011.png new file mode 100644 index 000000000..2039c6d1a Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10011.png differ diff --git a/static/pics/profile_photos/icons8-animal-10012.png b/static/pics/profile_photos/icons8-animal-10012.png new file mode 100644 index 000000000..e348a2466 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10012.png differ diff --git a/static/pics/profile_photos/icons8-animal-10013.png b/static/pics/profile_photos/icons8-animal-10013.png new file mode 100644 index 000000000..0d54a3e66 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10013.png differ diff --git a/static/pics/profile_photos/icons8-animal-10014.png b/static/pics/profile_photos/icons8-animal-10014.png new file mode 100644 index 000000000..a0283bdfe Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10014.png differ diff --git a/static/pics/profile_photos/icons8-animal-10015.png b/static/pics/profile_photos/icons8-animal-10015.png new file mode 100644 index 000000000..1f310a44d Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10015.png differ diff --git a/static/pics/profile_photos/icons8-animal-10016.png b/static/pics/profile_photos/icons8-animal-10016.png new file mode 100644 index 000000000..bd7fe39bc Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10016.png differ diff --git a/static/pics/profile_photos/icons8-animal-10017.png b/static/pics/profile_photos/icons8-animal-10017.png new file mode 100644 index 000000000..124b2a62d Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10017.png differ diff --git a/static/pics/profile_photos/icons8-animal-10018.png b/static/pics/profile_photos/icons8-animal-10018.png new file mode 100644 index 000000000..315695a34 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-10018.png differ diff --git a/static/pics/profile_photos/icons8-animal-1002.png b/static/pics/profile_photos/icons8-animal-1002.png new file mode 100644 index 000000000..2241c7997 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1002.png differ diff --git a/static/pics/profile_photos/icons8-animal-1003.png b/static/pics/profile_photos/icons8-animal-1003.png new file mode 100644 index 000000000..3936277ff Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1003.png differ diff --git a/static/pics/profile_photos/icons8-animal-1004.png b/static/pics/profile_photos/icons8-animal-1004.png new file mode 100644 index 000000000..c902b641c Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1004.png differ diff --git a/static/pics/profile_photos/icons8-animal-1005.png b/static/pics/profile_photos/icons8-animal-1005.png new file mode 100644 index 000000000..d9f28af0b Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1005.png differ diff --git a/static/pics/profile_photos/icons8-animal-1006.png b/static/pics/profile_photos/icons8-animal-1006.png new file mode 100644 index 000000000..7452e0d0c Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1006.png differ diff --git a/static/pics/profile_photos/icons8-animal-1007.png b/static/pics/profile_photos/icons8-animal-1007.png new file mode 100644 index 000000000..f80433055 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1007.png differ diff --git a/static/pics/profile_photos/icons8-animal-1008.png b/static/pics/profile_photos/icons8-animal-1008.png new file mode 100644 index 000000000..c5985f02b Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1008.png differ diff --git a/static/pics/profile_photos/icons8-animal-1009.png b/static/pics/profile_photos/icons8-animal-1009.png new file mode 100644 index 000000000..3e0c6a434 Binary files /dev/null and b/static/pics/profile_photos/icons8-animal-1009.png differ diff --git a/static/pics/profile_photos/icons8-bad-piggies-100.png b/static/pics/profile_photos/icons8-bad-piggies-100.png new file mode 100644 index 000000000..4fa947a9b Binary files /dev/null and b/static/pics/profile_photos/icons8-bad-piggies-100.png differ diff --git a/static/pics/profile_photos/icons8-badlion-100.png b/static/pics/profile_photos/icons8-badlion-100.png new file mode 100644 index 000000000..a67d1a731 Binary files /dev/null and b/static/pics/profile_photos/icons8-badlion-100.png differ diff --git a/static/pics/profile_photos/icons8-bowling-100.png b/static/pics/profile_photos/icons8-bowling-100.png new file mode 100644 index 000000000..ea54197c8 Binary files /dev/null and b/static/pics/profile_photos/icons8-bowling-100.png differ diff --git a/static/pics/profile_photos/icons8-cat-100.png b/static/pics/profile_photos/icons8-cat-100.png new file mode 100644 index 000000000..863f5010a Binary files /dev/null and b/static/pics/profile_photos/icons8-cat-100.png differ diff --git a/static/pics/profile_photos/icons8-cat-1001.png b/static/pics/profile_photos/icons8-cat-1001.png new file mode 100644 index 000000000..0e6bf1ab6 Binary files /dev/null and b/static/pics/profile_photos/icons8-cat-1001.png differ diff --git a/static/pics/profile_photos/icons8-danganronpa-100.png b/static/pics/profile_photos/icons8-danganronpa-100.png new file mode 100644 index 000000000..5070f4ad6 Binary files /dev/null and b/static/pics/profile_photos/icons8-danganronpa-100.png differ diff --git a/static/pics/profile_photos/icons8-dog-100.png b/static/pics/profile_photos/icons8-dog-100.png new file mode 100644 index 000000000..6e006cf52 Binary files /dev/null and b/static/pics/profile_photos/icons8-dog-100.png differ diff --git a/static/pics/profile_photos/icons8-dog-1001.png b/static/pics/profile_photos/icons8-dog-1001.png new file mode 100644 index 000000000..947b4db43 Binary files /dev/null and b/static/pics/profile_photos/icons8-dog-1001.png differ diff --git a/static/pics/profile_photos/icons8-dragon-100.png b/static/pics/profile_photos/icons8-dragon-100.png new file mode 100644 index 000000000..3da10904a Binary files /dev/null and b/static/pics/profile_photos/icons8-dragon-100.png differ diff --git a/static/pics/profile_photos/icons8-dragon-1001.png b/static/pics/profile_photos/icons8-dragon-1001.png new file mode 100644 index 000000000..771fb7a18 Binary files /dev/null and b/static/pics/profile_photos/icons8-dragon-1001.png differ diff --git a/static/pics/profile_photos/icons8-dragon-1002.png b/static/pics/profile_photos/icons8-dragon-1002.png new file mode 100644 index 000000000..ba5bfa62b Binary files /dev/null and b/static/pics/profile_photos/icons8-dragon-1002.png differ diff --git a/static/pics/profile_photos/icons8-european-dragon-100.png b/static/pics/profile_photos/icons8-european-dragon-100.png new file mode 100644 index 000000000..279379a12 Binary files /dev/null and b/static/pics/profile_photos/icons8-european-dragon-100.png differ diff --git a/static/pics/profile_photos/icons8-five-nights-at-freddys-100.png b/static/pics/profile_photos/icons8-five-nights-at-freddys-100.png new file mode 100644 index 000000000..4aec111a1 Binary files /dev/null and b/static/pics/profile_photos/icons8-five-nights-at-freddys-100.png differ diff --git a/static/pics/profile_photos/icons8-flappy-dunk-100.png b/static/pics/profile_photos/icons8-flappy-dunk-100.png new file mode 100644 index 000000000..e8ac1cce3 Binary files /dev/null and b/static/pics/profile_photos/icons8-flappy-dunk-100.png differ diff --git a/static/pics/profile_photos/icons8-game-100.png b/static/pics/profile_photos/icons8-game-100.png new file mode 100644 index 000000000..578f22da9 Binary files /dev/null and b/static/pics/profile_photos/icons8-game-100.png differ diff --git a/static/pics/profile_photos/icons8-gem-100.png b/static/pics/profile_photos/icons8-gem-100.png new file mode 100644 index 000000000..f1619d5c7 Binary files /dev/null and b/static/pics/profile_photos/icons8-gem-100.png differ diff --git a/static/pics/profile_photos/icons8-genshin-impact-100.png b/static/pics/profile_photos/icons8-genshin-impact-100.png new file mode 100644 index 000000000..22048f317 Binary files /dev/null and b/static/pics/profile_photos/icons8-genshin-impact-100.png differ diff --git a/static/pics/profile_photos/icons8-league-of-legends-100.png b/static/pics/profile_photos/icons8-league-of-legends-100.png new file mode 100644 index 000000000..a39857bd8 Binary files /dev/null and b/static/pics/profile_photos/icons8-league-of-legends-100.png differ diff --git a/static/pics/profile_photos/icons8-meowth-100.png b/static/pics/profile_photos/icons8-meowth-100.png new file mode 100644 index 000000000..6cf27b80c Binary files /dev/null and b/static/pics/profile_photos/icons8-meowth-100.png differ diff --git a/static/pics/profile_photos/icons8-minecraft-skeleton-100.png b/static/pics/profile_photos/icons8-minecraft-skeleton-100.png new file mode 100644 index 000000000..2366118da Binary files /dev/null and b/static/pics/profile_photos/icons8-minecraft-skeleton-100.png differ diff --git a/static/pics/profile_photos/icons8-minecraft-zombie-100.png b/static/pics/profile_photos/icons8-minecraft-zombie-100.png new file mode 100644 index 000000000..2aa2e3136 Binary files /dev/null and b/static/pics/profile_photos/icons8-minecraft-zombie-100.png differ diff --git a/static/pics/profile_photos/icons8-monopoly-100.png b/static/pics/profile_photos/icons8-monopoly-100.png new file mode 100644 index 000000000..8e43d2545 Binary files /dev/null and b/static/pics/profile_photos/icons8-monopoly-100.png differ diff --git a/static/pics/profile_photos/icons8-pacman-100.png b/static/pics/profile_photos/icons8-pacman-100.png new file mode 100644 index 000000000..978150e51 Binary files /dev/null and b/static/pics/profile_photos/icons8-pacman-100.png differ diff --git a/static/pics/profile_photos/icons8-prodigy-100.png b/static/pics/profile_photos/icons8-prodigy-100.png new file mode 100644 index 000000000..50de10ce3 Binary files /dev/null and b/static/pics/profile_photos/icons8-prodigy-100.png differ diff --git a/static/pics/profile_photos/icons8-royal-kingdom-crown-with-jewels-embedded-layout-100.png b/static/pics/profile_photos/icons8-royal-kingdom-crown-with-jewels-embedded-layout-100.png new file mode 100644 index 000000000..84a7a1fbe Binary files /dev/null and b/static/pics/profile_photos/icons8-royal-kingdom-crown-with-jewels-embedded-layout-100.png differ diff --git a/static/pics/profile_photos/icons8-style-100.png b/static/pics/profile_photos/icons8-style-100.png new file mode 100644 index 000000000..67a1d3b93 Binary files /dev/null and b/static/pics/profile_photos/icons8-style-100.png differ diff --git a/static/pics/profile_photos/icons8-the-binding-of-isaac-100.png b/static/pics/profile_photos/icons8-the-binding-of-isaac-100.png new file mode 100644 index 000000000..03857325d Binary files /dev/null and b/static/pics/profile_photos/icons8-the-binding-of-isaac-100.png differ diff --git a/static/pics/profile_photos/icons8-type-100.png b/static/pics/profile_photos/icons8-type-100.png new file mode 100644 index 000000000..dfba25f47 Binary files /dev/null and b/static/pics/profile_photos/icons8-type-100.png differ diff --git a/static/pics/profile_photos/icons8-ultra-ball-100.png b/static/pics/profile_photos/icons8-ultra-ball-100.png new file mode 100644 index 000000000..394613559 Binary files /dev/null and b/static/pics/profile_photos/icons8-ultra-ball-100.png differ diff --git a/static/pics/profile_photos/icons8-unturned-100.png b/static/pics/profile_photos/icons8-unturned-100.png new file mode 100644 index 000000000..e96b836b0 Binary files /dev/null and b/static/pics/profile_photos/icons8-unturned-100.png differ diff --git a/static/pics/winter.jpg b/static/pics/winter.jpg index 12294e417..2491e7954 100644 Binary files a/static/pics/winter.jpg and b/static/pics/winter.jpg differ diff --git a/static/stylesheets/button.css b/static/stylesheets/button.css new file mode 100644 index 000000000..979bd2f73 --- /dev/null +++ b/static/stylesheets/button.css @@ -0,0 +1,82 @@ +.start-button { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: #48bb78; /* bg-green-500 */ + padding: 0.5rem 1rem; /* px-4 py-2 */ + font-size: 0.875rem; /* text-sm */ + font-weight: 500; /* font-medium */ + color: white; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + transition: background-color 0.2s; /* transition-colors */ + outline: none; +} + +.start-button:hover { + background-color: #38a169; /* hover:bg-green-600 */ +} + +.start-button:focus { + outline: none; + box-shadow: 0 0 0 2px #f0fff4, 0 0 0 4px #48bb78; /* focus:ring-2 focus:ring-green-500 focus:ring-offset-2 */ +} + +.start-button:disabled { + cursor: not-allowed; + opacity: 0.5; /* disabled:cursor-not-allowed disabled:opacity-50 */ +} +.custom-mode-button { + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; /* Equivalent to whitespace-nowrap in Tailwind */ + font-size: 0.875rem; /* text-sm */ + background-color: #4299e1; /* bg-blue-500 */ + color: white; + font-weight: 500; /* font-medium */ + padding: 0.5rem 1rem; /* py-2 px-4 */ + transition: background-color 0.2s; /* transition-colors */ + outline: none; +} + +.custom-mode-button:hover { + background-color: #3182ce; /* hover:bg-blue-600 */ +} + +.custom-mode-button:focus-visible { + outline: none; + box-shadow: 0 0 0 2px transparent, 0 0 0 4px #4299e1; /* focus-visible:ring-2 focus-visible:ring-blue-500 */ +} + +.custom-mode-button:disabled { + pointer-events: none; + opacity: 0.5; /* disabled:pointer-events-none disabled:opacity-50 */ +} +.reset-button { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: #f56565; /* bg-red-500 */ + padding: 0.5rem 1rem; /* px-4 py-2 */ + font-size: 0.875rem; /* text-sm */ + font-weight: 500; /* font-medium */ + color: white; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow */ + transition: background-color 0.2s; /* transition-colors */ + outline: none; +} + +.reset-button:hover { + background-color: #c53030; /* hover:bg-red-600 */ +} + +.reset-button:focus-visible { + outline: none; + box-shadow: 0 0 0 1px #c53030; /* focus-visible:ring-1 focus-visible:ring-red-700 */ +} + +.reset-button:disabled { + pointer-events: none; + opacity: 0.5; /* disabled:pointer-events-none disabled:opacity-50 */ +} + diff --git a/static/stylesheets/leaderboard.css b/static/stylesheets/leaderboard.css index 8e918852e..ab94a201a 100644 --- a/static/stylesheets/leaderboard.css +++ b/static/stylesheets/leaderboard.css @@ -13,6 +13,11 @@ background-position: center; /* Center the background */ } +#leaderboard table tbody img { + display: inline-block; + width: 20%; +} + #leaderboard th { padding: 12px 20px; /* Adjust padding to reduce space around the text and icon */ text-align: center; diff --git a/static/stylesheets/menu.css b/static/stylesheets/menu.css index bed88466f..8d2f1d86d 100644 --- a/static/stylesheets/menu.css +++ b/static/stylesheets/menu.css @@ -3,28 +3,28 @@ width: 100%; display: flex; flex-direction: column; - justify-content: center; /* Align items to the center of the page */ + justify-content: center; align-items: center; - padding: 4rem 20% 4rem; /* Increase top and bottom padding */ - row-gap: 2rem; /* Increase the gap between rows */ + padding: 4vh 20vw; + row-gap: 2rem; } -#menu-ctn h1 { - width: calc(100% - 20px); - text-align: center; - border-bottom: 1px solid #294E95; /* Change border color and decrease thickness */ - max-width: 900px; /* Increase maximum width */ +#title-container { + width: 100%; /* Ensure the container takes full width */ + text-align: center; /* Center the text */ + max-width: 900px; /* Limit the maximum width */ + margin-bottom: 20px; /* Add some space between the title and other elements */ } -#menu-ctn #menu { +#menu { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-rows: auto auto; - row-gap: 2rem; /* Increase the gap between rows */ - column-gap: 4rem; /* Increase the gap between columns */ - justify-content: center; /* Align buttons to the center of the page */ + row-gap: 2rem; + column-gap: 4rem; + justify-content: center; } .menu-item button img { - margin-bottom: 20px; /* Increase space between image and text */ + margin-bottom: 20px; } diff --git a/static/stylesheets/typeInterface.css b/static/stylesheets/typeInterface.css index ef5cf9307..ef71dd2fd 100644 --- a/static/stylesheets/typeInterface.css +++ b/static/stylesheets/typeInterface.css @@ -1,83 +1,86 @@ #game-container { - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 20%; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20%; } #game-container h1 { - margin: 1rem auto; + margin: 1rem auto; } #game-container #startBtn { - margin: 1rem auto; + margin: 1rem auto; } #text-display { - font-size: 18px; - margin-bottom: 20px; - white-space: pre-wrap; - word-wrap: break-word; + font-size: 18px; + margin-bottom: 20px; + white-space: pre-wrap; + word-wrap: break-word; } #input-box { - padding: 10px; - font-size: 16px; - width: 80%; - /* Adjust the width as needed */ + padding: 10px; + font-size: 16px; + width: 80%; + /* Adjust the width as needed */ } #result { - margin-top: 20px; - font-weight: bold; + margin-top: 20px; + font-weight: bold; } .typed-word { - color: green; + color: green; } .dropdown { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .dropdown-content { - display: none; - position: absolute; - background-color: #f9f9f9; - min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 1; + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; } .dropdown-content button { - color: black; - padding: 12px 16px; - text-decoration: none; - display: block; - background-color: inherit; - border: none; - width: 100%; - text-align: left; + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; + background-color: inherit; + border: none; + width: 100%; + text-align: left; } .dropdown-content button:hover { - background-color: #f1f1f1; + background-color: #f1f1f1; } .dropdown:hover .dropdown-content { - display: block; + display: block; } .dropdown:hover .dropbtn { - background-color: #3e8e41; + background-color: #3e8e41; +} +.dropdown:hover .dropbtn.custom-mode-button:hover { + background-color: #4299e1; /* Keep the original color */ } #holder { - width: 500px !important; - height: 500px !important; - padding: 50px; + width: 500px !important; + height: 500px !important; + padding: 50px; } diff --git a/templates/About.html b/templates/About.html index e31226615..e0164c90a 100644 --- a/templates/About.html +++ b/templates/About.html @@ -4,7 +4,7 @@ About ThrillTyper - + diff --git a/templates/base.html b/templates/base.html index e6f1b623a..1a3b18b83 100644 --- a/templates/base.html +++ b/templates/base.html @@ -28,12 +28,13 @@ - + + {% block head %} {% endblock %} diff --git a/test.py b/test.py index 7ac35ab84..9b257f0a9 100644 --- a/test.py +++ b/test.py @@ -223,13 +223,13 @@ def test_data_table(self, sample_user_info, cleanup): random_user_data = { '_username': sample_user_info['_username'], '_email': sample_user_info['_email'], - '_history_highest_race_wpm': random.randint(50, 100), + '_top_wpm': random.randint(50, 100), '_accuracy': round(random.uniform(80, 100), 2), '_wins': random.randint(0, 100), '_losses': random.randint(0, 100), '_freq_mistyped_words': '|'.join(random.choices(["word1", "word2", "word3", "word4", "word5"], k=3)), '_total_playing_time': random.randint(0, 1000), - '_play_date': datetime.now() + # '_play_date': datetime.now() } Database.insert(UserData, **random_user_data) @@ -239,7 +239,7 @@ def test_data_table(self, sample_user_info, cleanup): # Assert individual attributes of user_data object assert user_data._username == sample_user_info['_username'] assert user_data._email == sample_user_info['_email'] - assert user_data._history_highest_race_wpm == random_user_data['_history_highest_race_wpm'] + assert user_data._top_wpm == random_user_data['_top_wpm'] assert float(user_data._accuracy) == random_user_data['_accuracy'] assert user_data._wins == random_user_data['_wins'] assert user_data._losses == random_user_data['_losses'] diff --git a/text_generator.py b/text_generator.py index d07453e26..7e8c186bb 100644 --- a/text_generator.py +++ b/text_generator.py @@ -1,15 +1,17 @@ from random import randint LEN_OF_LONGEST_WORD = 22 + + class Text_Generator: """ Responsible for generating text for a game to use and also separating words into different difficulties (the latter is done outside of run time) """ LEFT_SIDE = "qwert|asdfg|zxcv" LEFT_ROW2_START = LEFT_SIDE.find('|') - LEFT_ROW3_START = LEFT_SIDE.find('|',LEFT_ROW2_START+1) + LEFT_ROW3_START = LEFT_SIDE.find('|', LEFT_ROW2_START+1) RIGHT_SIDE = "poiuy|lkjh|mnb" RIGHT_ROW2_START = RIGHT_SIDE.find('|') - RIGHT_ROW3_START = RIGHT_SIDE.find('|',RIGHT_ROW2_START+1) + RIGHT_ROW3_START = RIGHT_SIDE.find('|', RIGHT_ROW2_START+1) PINKIE_CHARS = "qaz" def get_txt_list(file) -> list[str]: @@ -17,57 +19,57 @@ def get_txt_list(file) -> list[str]: Reads from wordList.txt to create an array of the words in it :return wordList : list of words contained in wordList.txt """ - txtListFile = open(file,"r") + txtListFile = open(file, "r") return txtListFile.read().split('\n') - + def get_avg_txt_len(lst): - lenWordsLst = list(map(len,lst)) + lenWordsLst = list(map(len, lst)) return sum(lenWordsLst)/len(lenWordsLst) - def score_word_typing_difficulty(self,word)->int: + def score_word_typing_difficulty(self, word) -> int: """ Scores words according to their typing difficulty :param word : return score """ score = 0 - if len(word)<=3: + if len(word) <= 3: return 0 i = 0 side_switches = 0 direc_verts = 0 - while i5: - score+=(side_switches-5)*0.25 - side_switches=0 - if self.is_direct_vertical(curr_word_left_ind,next_word_left_ind, True): - direc_verts+=1 - elif self.is_direct_vertical(curr_word_right_ind,next_word_right_ind, False): - direc_verts+=1 - i+=1 - if direc_verts>2: - score+=(direc_verts-3)*0.25 - if side_switches>5: - score+=(side_switches-5)*0.25 + if side_switches > 5: + score += (side_switches-5)*0.25 + side_switches = 0 + if self.is_direct_vertical(curr_word_left_ind, next_word_left_ind, True): + direc_verts += 1 + elif self.is_direct_vertical(curr_word_right_ind, next_word_right_ind, False): + direc_verts += 1 + i += 1 + if direc_verts > 2: + score += (direc_verts-3)*0.25 + if side_switches > 5: + score += (side_switches-5)*0.25 return score/(LEN_OF_LONGEST_WORD+1-len(word))*100 - - def is_direct_vertical(self,curr_char_keyboard_pos, nxt_char_keyboard_pos, is_left): + + def is_direct_vertical(self, curr_char_keyboard_pos, nxt_char_keyboard_pos, is_left): """ Determines whether keys are directly vertically above or below each other @precondition : the characters are on the same half (left or right) of the keyboard @@ -75,86 +77,93 @@ def is_direct_vertical(self,curr_char_keyboard_pos, nxt_char_keyboard_pos, is_le :param nxt_char_keyboard_pos : index of the next character in the representation of the left half of the keyboard if is_left or RIGHT_SIDE otherwise :param is_left : boolean that indicates whether the char belongs to """ - if (curr_char_keyboard_pos!=-1 and nxt_char_keyboard_pos!=-1): - #standardize the rows + if (curr_char_keyboard_pos != -1 and nxt_char_keyboard_pos != -1): + # standardize the rows row2_start = self.RIGHT_ROW2_START row3_start = self.RIGHT_ROW3_START if is_left: row2_start = self.LEFT_ROW2_START row3_start = self.LEFT_ROW3_START - if curr_char_keyboard_pos>row3_start: - curr_char_keyboard_pos-=row3_start - elif curr_char_keyboard_pos>row2_start: - curr_char_keyboard_pos-=row2_start - if nxt_char_keyboard_pos>row3_start: - nxt_char_keyboard_pos-=row3_start - elif nxt_char_keyboard_pos>row2_start: - nxt_char_keyboard_pos-=row2_start - return True if abs(curr_char_keyboard_pos-nxt_char_keyboard_pos)<=2 else False + if curr_char_keyboard_pos > row3_start: + curr_char_keyboard_pos -= row3_start + elif curr_char_keyboard_pos > row2_start: + curr_char_keyboard_pos -= row2_start + if nxt_char_keyboard_pos > row3_start: + nxt_char_keyboard_pos -= row3_start + elif nxt_char_keyboard_pos > row2_start: + nxt_char_keyboard_pos -= row2_start + return True if abs(curr_char_keyboard_pos-nxt_char_keyboard_pos) <= 2 else False else: return False - def sort_words_by_difficulty(self,word_lst:list[str]): + def sort_words_by_difficulty(self, word_lst: list[str]): """ Uses the scoring function to score each of the words in the given word list and then split them off to different files based on their difficulty :param word_lst """ easy = "" - easy_count=0 + easy_count = 0 medium = "" - med_count=0 + med_count = 0 hard = "" - hard_count=0 + hard_count = 0 num_words = 0 total = 0 for word in word_lst: score = self.score_word_typing_difficulty(word) - num_words+=1 - total+=score - if score<=1.5: - easy+=word+'\n' - easy_count+=1 - elif score<3.2: - medium+=word+'\n' - med_count+=1 + num_words += 1 + total += score + if score <= 1.5: + easy += word+'\n' + easy_count += 1 + elif score < 3.2: + medium += word+'\n' + med_count += 1 else: - hard+=word+'\n' - hard_count+=1 + hard += word+'\n' + hard_count += 1 print(f"Average: {total/num_words}") print(easy_count) print(med_count) print(hard_count) - with open("easy_words.txt","w") as easy_words: + with open("easy_words.txt", "w") as easy_words: easy_words.write(easy.strip('\n')) - with open("medium_words.txt","w") as medium_words: + with open("medium_words.txt", "w") as medium_words: medium_words.write(medium.strip('\n')) - with open("hard_words.txt","w") as hard_words: + with open("hard_words.txt", "w") as hard_words: hard_words.write(hard.strip('\n')) - def generate_text(difficulty:str,form:str,amount:int): + def generate_text(difficulty: str, form: str, amount: int, genre: str = None): """ - Generates the text that shall be typed by users for a game + Generates the text that shall be typed by users for a game. + If 'genre' is specified, it modifies the file selection process, + otherwise, the file is selected based on 'difficulty' and 'form'. """ - file = None + file_name = "" try: - if difficulty: - difficulty+="_" - with open(f"{difficulty}{form}.txt",'r') as file: + # Determine the file name based on whether 'genre' is provided + if genre: + file_name = f"{genre}{form}.txt" + elif difficulty: + file_name = f"{difficulty}_{form}.txt" + else: + return "Difficulty must be specified if genre is not provided." + + with open(file_name, 'r') as file: + txt_lst = file.readlines() + # Ensure 'amount' does not exceed number of lines available + amount = min(int(amount), len(txt_lst)) otpt = "" - txt_lst=file.readlines() - n = len(txt_lst) - amount = int(amount) - for i in range(amount-1): - rand_ind = randint(0,n-1) - otpt+=txt_lst.pop(rand_ind).replace('\n',' ') - n-=1 - rand_ind = randint(0,n-1) - otpt+=txt_lst.pop(rand_ind).replace('\n','') - return otpt + for i in range(amount): + rand_ind = randint(0, len(txt_lst)-1) + # Using strip to remove newline characters + otpt += txt_lst.pop(rand_ind).strip() + ' ' + return otpt.strip() # Remove the last space except Exception as e: - print(e) - return "Invalid arguments or missing arguments." + print(f"Error: {e}") + return "An error occurred, check the file name and the parameters." + -if __name__=="__main__": - tg=Text_Generator() - tg.sort_words_by_difficulty(tg.get_txt_list("words.txt")) \ No newline at end of file +if __name__ == "__main__": + tg = Text_Generator() + tg.sort_words_by_difficulty(tg.get_txt_list("words.txt"))