Video Demo: video link
Final Project for the CS50 Introduction to Python.
Askar Mukhanov
The project.py and test_project.py
are there just to pass the structure of cs50 submit.
An actual project is flask app that is intiatialized in run.py with create_app() .
I consulted with ai tools only with the goal of learning, the questions that were asked I self restricted into a format of explainig some global topics or very scoped single function problems mostly with JavaScript.
I havent requested any fully done solutions, although there might be some ideas, that seeped into my mind unintentionally, due to sometimes ChatGPT would be too "chatty".
Initial idea for this project was to make use of Regular Expression. It was fascinating subject to learn that type of searching algoriths. As a basis for pattern to parse through I've decided to use an aspect of the game Tekken which is Notations, specifically how to write its individual moves and combos, as it has quite a strict ruleset for separating each move with different values.
The game offers a vast library of moves for each game character, and those moves can be stringed together with consequitive or simulteneous button/arcade stick inputs:
Types of inputs include:
- Basic attack buttons
- Combination of attack buttons
- Basic movement input
- Special movement input
- Base and unique stances for each character
On top of that character can be in different state:
- Various state of knockdown
- being thrown
- Stunned
- etc
For many Tekken enthuasists it can be difficult to share knowledge due to complexity and confusion of how to write notation correctly with tekken lingo.
This app was built with intent to simplify this process.
Parsing through a given string of text, and if written according to ruleset it will generate an image of a combo in .png format for easier recognition.
Convert with [Text to Image] button
Convert with [Image to Text] button
Users can also share their comboes on twitter like feed, if they make an account.
Originally it was done in CLI interface and would ask for a string of combo and would return either a .jpeg image of a combo or a .pdf file with an image, original notation, parsed through notation with background of chosen character.
However very quickly I realized two problems with that approach.
- No one outside of coding knows how to use a terminal
- It is inconvinient to share your results
So I had a choice either a traditional application with some basic GUI using PyQt / Tkinter package or something that will work straight out of a browser. Due to nature of my goal comboes being shareble, I chose web app approach.
Another aspect that pushed me to use web approach was the fact that in past I tried to build flask app, but failed miserably. So I've decided to use flask framework for this assignment.
I didn't know that this would be a rabbit hole that I'd be falling for the last 3-4 months...
The structure of this project was done with the uv package manager. I learned about it half way through the project.
pyproject.toml contains all of the dependecies.
This app launches from run.py file and the app itself is in the package pythonblog that itseslf is segmented into blueprints.
In pythonblog package I have __init__.py which instantiate my app itself, SQL database, and objects from libraries that will be used throught this project:
mail = Mail()
bcrypt = Bcrypt()
login_manager = LoginManager()the app is instantiated with a function create_app(config_file)
which takes Path as an argument using method from Flask .config.from_pyfile() and instantiate inddividual blueprints with .register_blueprint()
Configuration are in config.py file and used using environmental variables that are accessed with os.
models.py Contains my database models which inherit functionality from SQLAlchemy
And Serializer from itsDangerous that Im using for getting tokens for reset password functionality.
Two main modules of this app are main and users.
templates as the name suggests, contains all of the htmls that used by Jinja2 templating engine.
static is where all my assets are.
errors and posts are minor features of this web app, that handle individual common erroneous server responses and individual posts routing.
main package contains routes and functionality related to my combo parsing methods, as well as forms that are used to submit data to db or for POST methods to go through my parsing "algorithms".
In routes.py I have only 2 routes:
/about route:
- renders an "About" html
/home route(duplicates to / )
what it does:
- cleans a folder holding user generated combos
- accessing required assets with
Combo.get_assets()andCombo.open_scv() - instantiate variable passed from forms
- based on
"GET"or"POST"request.method does differnt things- if method was
"GET"- simply renders a template with passing
postsvariable torender_template()which contains paginated object from Post model
- simply renders a template with passing
- however if method was
"POST"- depending which id it got with
request.form.get("action")- tries to form data to db if its validated
- except there is an error;; then it return a
flash()and passing torender_template()
- tries to generate list from a form data string
- using
Combo.reverse_parse()generates a new string and passing it to form.data - except there is an error; then it returns flash()
- gets a string for form data
- parsing it with
Combo.combo_parse() - makes an image with
Combo.make_image() - makes a list to pass to
render_template()which will be used injsfor dynamicaly generated image
- renders a template with
postsvariable which contains paginated object from Post model
- depending which id it got with
- if method was
In combo.py I have:
combo_parse(combo)- accepts a string of individual tekken moves that is separated by
single white spaceor. - opens csv file from assets and gets indidual character stances or moves and puts them into a list
special_stance=[] - splits that string into individual moves and checks them for "validity" with this pattern
pattern = re.compile( rf"^(T!|{special_stance}|{stance}|(?:{motion})?|{motion}{button_input}|({button_input}[~]{motion})|{button_input}|{button_input}*)$" ) - returns a list of validated parts with
nextin between each move
- accepts a string of individual tekken moves that is separated by
make_image(combo_parsed, combo_name)- takes a list that was returned from
combo_parse()and unique char name that was passed from submit form - combines all moves in list using
PILmodule creates a.pngimage with a traansparent abckground and random name usingsecrets.token_hex()
- takes a list that was returned from
make_preview(combo_parsed)- takes a list of parsed results and based on tekken lingo adds correct symbols to each move
- returns a new list with finalized correct combo
reverse_parse(form_list)- takes a list of moves passed from form
- using while loop and establishes a counter that will expire on end of list
- based on previous and next move, transform current move according to tekken lingo
- returns a string to be paassed to form data field
clean_folder()- cleans a directory containig made combos
get_assets()- returns a list of assets to pass used that is passed to
render_template()
- returns a list of assets to pass used that is passed to
open_csv()- returns a list of individual moves that is passed to
render_template()
- returns a list of individual moves that is passed to
users module handles everything related to Users activities.
It contains routes.py, functions.py and forms.py.
routes.py includes such routes as :
/register- if
current_user.is_authenticated- redirects to
/home
- redirects to
- if form checks validation with
.validate_on_submit()- instantiate a variable using
bcryct.generate_password_hash()based onform.password.datafield - passes a path to a default image which will be used by
User() - instantiate a user
User()model and commits it to database - passing
flash()message of success torender_template() - redirect to
/loginroute
- instantiate a variable using
- otherwise (method was
"GET") renders a template
- if
/login- if
current_user.is_authenticated- redirects to
/home
- redirects to
- if form checks validation with
.valiate_on_submit()- instantiate a user variable using
User()model which queries db by first foundUserobject identical to form.email.data - checks a hash with
bcrypt.check_password_hash() - log-ins a user with
login_user()fromflask_login - redirects to
home
- instantiate a user variable using
- otherwise
flash()passes "unsuccesful" message torender_template()
- if
/logout- logs-out a user with
logout_user() - redirect to
/home
- logs-out a user with
/account- requires a check with decorator
login_required - on
"GET"method- renders an account html passing to form.data value from
current_user.usernameandcurrent_user.email
- renders an account html passing to form.data value from
- on
"POST"method- checks validity with
.valdiate_on_submit()- instantiate a hexed name with
save_picture(form.picture.data)which will be a profile picture of the user instead of default - commits current user to db
- passes a positive
flash()message torender_template()
- instantiate a hexed name with
- checks validity with
- requires a check with decorator
/user/<string:username>- instantiates a user with
User()model that was got withfirst_or_404()method - instantiates a Paginated
Post()obbject that was determined bybackref,author=user - renders a template with passed variables
- instantiates a user with
/request_reset- if current_user.is_authenticated
- redirects
/home
- redirects
- otherwise checks validity of the form with
.validate_on_submit()- instantiates a
User()objectquery.filter_by(email=form.email.data) - sends an email with Serializer object with
send_email() - redirects to
/login
- instantiates a
- if current_user.is_authenticated
/reset_password/<string:token>- checks if
current_user.is_authenticated- redirects to
/home
- redirects to
- instantiate a user with
User.verify_reset_token(token) - if form validates with
.validate_on_submit()- generates a hashed password with
bcrypt.generate_password_hash(form.password.data) - commits a user to db
- redirects to
/login
- generates a hashed password with
- checks if
functions.py contains just two functions that support routing above
-
send_email()- gets a
sender=from environment variable forrmsg - generates a Serializer and send an email with that
tokenusingmail.send(msg)
- gets a
-
save_picture(form_picture)- generates a random name with
secrets.token.hex() - tranforms an image from
form_pictureusingImagemodule fromPILto a thumbnail to save space - saves it to assets folder
- returns a path to that image
- generates a random name with
These forms handle most of "data transfer" between front and backend of my app
They all inherit from Super which is FlaskForm
These forms are constructed with flask_wtf and wtforms, including various validators.
RegistrationForm()- its fields
usernameemailvalidates withEmail()passwordconfirm_passwordvalidates withEqualTo()submit
- additional functions
validate_username()andvalidate_email(). Those useUser.query()that filter by username and email and if user already exist raisesValidationError()
- its fields
LoginForm()- its fields
emailvalidates withEmail()passwordremembersubmit
- its fields
UpdateAccount()- its fields
usernameemailvalidates withEmail()pictureaccepts on.jpgand.pngfiles withFileAllowed()validatorsubmit
- has same additional functions as
RegistratioForm
- its fields
RequestResetForm()- its fields
emailvalidates withEmail()submit
- has addition same function
validate_email()
- its fields
ResetPasswordForm()- its fields
passwordconfirm_passwordvalidates withEqualTo()submit
- its fields
All htmls were constructed with the use of Bootstrap as well as my owb custom css that handles some unique buttons, backround, and other some divs that I wanted to to have everything nested inside follow same design pattern.
They are following a layout.html using jinja2 extends and {% blockcontent %} {% endblock %} .
All the pages have this simple structure and having forms=forms that were passed to render_template() constructed with jinja {{ }} .
Exception to that is home.html , due to having of mixture of handcrafted <form> and jinja2 constructed forms with {{ }}
its end up a pretty messy page.
While i tried to structure as many of my html code in readable/"neat" format, or loop for some repeated content like so:
<ul class="dropdown-menu">
{% for char in char_moves %}
<li>
<a type="button" onclick="set_char('{{ char["moves"]}}')" class="dropdown-item">
{ char["char"] }}
</a>
</li>
{% endfor %}
</ul>Due to my lack of knowledge of JavaScript, HTML + Jinja inserted functions like {{ url_for('static' , filename='')}}I got confused real fast with multi -layered quotations.
Beside that I failed to organize my grids row/columns in order how I wanted them to be as a result I end manually writing a lot of it.
<div class="col col-sm-auto">
<div class="container-fluid shadow-lg rounded p-3 mt-3 mb-3 mr-3 ml-3">
<div class="row row-cols-auto">
<div class="col p-0">
<!-- top motion -->
<a class="btn btn-tekken" onclick="add_image('{{ url_for('static', filename='assets/ub.png')}}')">
<img src="{{ url_for('static', filename='assets/ub.png')}}" alt="">
</a>
</div>
<div class="col p-0">
<!-- top motion -->
<a class="btn btn-tekken" onclick="add_image('{{ url_for("static", filename="assets/u.png") }}')">
<img src="{{ url_for('static', filename='assets/u.png')}}" alt="">
</a>
</div>
<div class="col p-0">
<a class="btn btn-tekken" onclick="add_image('{{ url_for("static", filename="assets/uf.png")}}')">
<img src="{{ url_for('static', filename='assets/uf.png')}}" alt="">
</a>
<!-- top motion -->
</div>At first this was planned ahed to be done without using scripts since I didn't know much about them, however by the I finished with my combo_parse() functionality and I started implementing reverse_parse() after adding each button my html page would reload and keep sending "POST".
So JS it is then.
I wasn't planning to starting to learn from basics and just jumped to documentation of how some of the Classes with JS work, and use of constructor. Some of it was mostly intuitive and reminiscent of Python, a lot of it was very confusing.
This part of my assignment was done with a lot of help from my forums and use of ChatGPT, Copilot(which proven to be the dumbest) and CS own AI duck . However as mentioned above I tried to restrict both myself and ai chat to provide answers in "teaching" manner and without handout solutions.
At first scripts were done at the bottom of my home.html, but they got out of the control real quick and I moved to its own file.
Every function is created in the same manner declare a const and construct a function, a lot of tutorials use moder method with -> akin to list comprehension, which proven too confusing if one doesn't knwo basics, so they all constsruted "old way"
Here are some of the functions:
add_image(path)- this function targets a
divby itsidand append to that div a childimgwhichsrc=would be a path given with jinjaurl_for() - then adds that path to a global const array called
list_pass, this array will be used in another function - this function was used a in almost all handcrafted buttons that trigger this function with
onclick=""
- this function targets a
send_list()- this function targets a hidden form
input - "puts"
list_passinto that form - afterwards that input form has a unique name that will trigger specific control flow that will use
reverse_parse()on that value/data of that form
- this function targets a hidden form
set_char(moves)- target a
divby itsid - this function accepts a string
- splits it by
,thus creating an array - using
forEachcreates abuttonelement that will have multiple required attributes specificallyonclick=""with mentioned above functionadd_image()and path that is needed for that function, will be constructed out of each item in created array string - then adds that new
buttonelement to a targeteddiv
- target a
- there are other small supporting functions that do little QoL things:
copy_mail()copy my email in About pagedelete_last()clears lastimgin previewdivdelete_all()clears all images in previewdivclear_field()clears input formfill_field()an old function that was done beforeadd_image(), planning to use it in another wayleft_key_click()event listener that triggers a button by its idright_key_click()~
- Implement URL parsing
- give URL and it gives back every matching to my combo regex
- Integration to Discord or Twitch
- maybe*
Half way through a project things got very complicated and my backup methods were not sufficient and I learned some basics of GIT VSC, as well got a bit more familiar with github.
I'm proud what with the results of this project, I have solidified lessons I learned from the CS50 course, concepts of different loop, that I understood before, now i have a solid grasp upon. The OOP structure that I had difficulties with getting on, now a lot more clearer.
What I've learned:
-
Working with documentations
-
Working with my Text Editors and IDE, throught this I tried PyCharm, VSCode and Sublime Text
-
Working with Terminal(I'm not afraid anymore🙃)
-
Working with different packages:
pathliband construstion of Pathsreospillowsecretstimedate
-
Flask framework
- Clear understanding of how routes work
- Templating
- Basic HTML
Mailmodulewtforms- Login system
request,url_forandredirect- tokenization of sensitive data
-
Custom CSS and Bootstrap
-
RE in general, I started to use it in my daily activities
-
I got better understing of SQL with sql-alchemy
- I had prior experience with MYSQL, but it was indirect and I didn't understand how queries work
-
OOP approach
- I got more comfortable with creating Classes and use of methods and objects (not so much with decorators)
-
Minor introduction into Javascript
-
Use of environmental variables
-
Clear understanding of basics of version control with GIT CLI
On the side note I tried Linux and I don't think I'm coming back to Windows. Thanks for all the fish🐬
This project was based on my files of the first attemp of learning Python, at a time I naively assumed flask is basics of Python. I was following the tutorial of Corey MS in his Flask Course, this was 2-3 years ago and that turorial is 7-8 years old now. That tutorial I believe is based on flask 1.0 release.
I was contemplating if this violates the ethical rules of cs50 course before starting this project, however by the end of it, I personally believe the goal of actually learning through practical approach was achived for me.
That said here are the giants:






