Unorganised Thoughts

Back

Fantasy SF6

My ideal job is to become a Python full-stack developer. This project? A Python full-stack Windows application. Take a look at what the app is, and how it was made!

17th of April 2026

Street Fighter 6 (SF6) is a competitive eSport, and for the last 30+ years, competitors from around the world have traveled globally to compete and claim the title of the absolute best. A "Fantasy League" is a meta game often played in football, Formula 1, basketball, or other sports circles, and is a game in which players choose real sports athletes from different teams to form their own imaginary team, and then win points based on the performance of the athletes in real games[1]. Fantasy SF6 combines these two ideas, and lets users create their dream SF6 teams and have them compete and score throughout the current season. This blog post will show off the project, as well as discuss some problems faced and their solutions

Firstly, I'd like to begin with how this app came to be. Around 5 months ago, I first had the idea to create a sort of Fantasy League for me and my friends to all partake in. It started incredibly simple and very "back of the napkin" so to speak; it was a simple spreadsheet with a few pages for players, scores, and rules. The plan was for me to track it all manually, and keep the spreadsheet in a public online drive everyone could view. While working on it however, I had the idea to create a script that simply pulled and then opened the latest version of the file in a read-only mode. This not only allowed me to keep unapproved edits at bay, but made for a smoother overall experience. Little did I know, this idea would cascade, and slowly but surely I kept having more and more ideas revolving around making the user experience for my friends easier until eventually I had realised: I was planning a standalone application.

It was decided ultimately, that I would create an app that I could share with not only my friends, but with the general public, to allow everyone to host their own fantasy leagues with their friends. So, I started planning a rough specification sheet, with notes on what the app must, should, and could do. Some of these features include:

  • Online league-joining and team-creating services.
  • A way to view all events that would count towards the league.
  • A leaderboard system that compares players in a league.
  • Mid-season trading between players
  • It did seem quite daunting at first, to implement all of these; after all I had no experience making a Windows application, let alone one that connected to a database somewhere and pulled all its data from the cloud. But I continued, and once a rough specification was drawn up, I had a solid understanding of what would need to be created. Namely, I would first create the database that was both inevitable and pivotal to the project, then, the Python backend to interact with the database, and finally the frontend to interact with the backend. I decided on Supabase to host my database, not only for its free price and simplistic nature, but also due to it having a built in API with features such as row-level security and built in OAuth with email signups. Python would be used for both the backend and frontend, not just because of my personal bias towards it, but also because it was by far the language in which I had the most experience; the last thing I wanted to deal with on top of all of this was learning a new language.

    So, after a month of planning, four months of development flew by, which brings us to today: the app's official v1.0.0 release! I cannot wait to watch the app's userbase grow, more features to be implemented, and to have the community buzzing about their leagues! I'm sure you're wondering at this point: what exactly can the app do? Well, let me show you!

    The current league page. The first draft of the league page.
    The final league page, including realtime messaging, a league history, and a cleaner aesthetic. / The first working draft of the league page, in all its Windows XP glory.

    This page went through the most changes. Aside from the very clear aesthetic differences, I was working on this page right up until the final release deadline. Initially, there would be separate league and team pages, but with that idea they both looked quite visually empty. After combining them, the problem was remedied slightly, but the issue still remained: there's just not much a user needs to see! The final fix, all thanks to a passing comment made by a friend helping me test, was to use the dead space to create a league feed. The league feed would contain info such as league joins, leaved, and chat messages. The realtime system was actually quite simple to implement. Supabase provides trigger functions that run on database inserts, so by creating a simple backend method that allowed users to insert chat messages into a league_chat table, a trigger function would be activated that sent a realtime broadcast to the all users within the league. Then came hooking up the realtime listener in the client, which proved incredibly difficult. Not for any reasons such as a complicated implementation or very verbose methods, just that it wouldn't work! Eventually however, I managed to bang my head against the wall enough until I got it working using the websockets asyncio package. The listener would connect whenever the league page was active, and on any ping, would update the league feed with the message. This implementation was great because it then allowed me to expand the league feed by just creating multiple triggers that activated on different database inserts, such as drafting a player or leaving a league.

    Rome wasn't built in a day, but this system was. It is by far the most necessary feature in making the app feel alive and exciting, and I am forever indebted to my friend "Applepie", who came up with the idea!

    The leaderboards page
    The leaderboards page, where you can view your league standings, leaguemate rosters, the player pool, and some global stats.

    The leaderboards page was incredibly simple. A team's points was equal to the sum total points of all their players, both former and current, and we could display the rosters just as easily. I will take this opportunity however to dive into how the entire player system works.

    Three tables control the player and team system: players, teams, and team_players. The players table is the most straightforward: a column for name, region, and total points, with each row representing one table. Since names are unique within the sport, I decided that would suffice as a primary key. Although slightly poor practice, the system worked flawlessly regardless. Next was the teams table. Again, a simple table with columns to represent the team ID, owner, name, date created, and the league the owner is in. While the league foreign key relationship was perhaps unecessary, after all a league and team can be linked just via the owner, it was implemented to speed up queries and keep the relationship between teams and leagues obvious.

    Then comes the big one, the team_players table. This table stores every instance of a player on a team, with the most notable columns storing points, date joined, and date left. Tieing points to player instances was ultimately the best choice, as it supported a system in which players come and go, but points don't, and allowed for easy calculating of point totals (more on this later). Drafting a player would append a row with a date joined set to the current time, and trading away a player would then update their left date column. A user's current team could easily be retrieved by grabbing all their players where their left date is null. The end result was a table that supported dynamically changing rosters that was resistant to any weird scoring issues. There were a couple ways to design this system, but in the end this design proved to be robust enough to function with ease.

    The events page. The standings for an event.
    The events page, which allowed for browsing and searching of all scoring events. / The results page for an event, which displayed the top rankings and the points in which those players scored.

    Events were also incredibly simple to implement. One table, events handles all the different events of the season, and a complete boolean column would define whether or not an event's scores had been processed, which brings us on to how scoring even works.

    score_history was a table that managed all the standings for all the events. After an event had completed, an admin would use a simply Python Command Line Interface (CLI) to upload the rankings for that event. Each row in this table consisted of the player and event as foreign key references, as well as a rank and points column; where rank was determined in the CLI via an ordered list, and points was determined by finding the event's tier and grabbing the correct point distribution from a separate table.

    Uploading standings would determine an event complete, as well as trigger a function to update the scores. This function would:

  • Iterate over the team_players table.
  • Determine each player's score using their joined and left dates as their tenure.
  • For each score_history entry, append it to a player's score if they were active during that event,
  • Update the total points column in every row in the players table.
  • This system worked flawlessly. Standings are uploaded which triggers scores to be updated, all in an atomic transaction to prevent any issues. its important to note however that this function wouldn't add scores, it would recalculate them. This is a double-edged sword, as while it ensures that at any moment's notice, incorrect standings could be updated and recalculated with no issue, it also meant the function could become quite slow over time despite consisting of mostly simple addition. This was intentional, I wanted to have full security knowing that, no matter what went wrong, scores could always be recalculated and fixed.

    The trades page
    The trades page, where five times a season, a trade window would open, allowing for up to two trades with fellow leaguemates or the open market.

    Finally, the trades page. This one wasn't difficult per se, but required a lot of fine tuning to avoid race conditions with two users attempting to trade the same player simultaneously. Simple tables for open trade requests and trade history were created, but interacting with these tables was the main challenge. I had to create most of the backend methods actually within the database, with the client-side methods being used to simply call those. Anything that actually interacted with the trades, such as accepting a request, deleting a request, or nabbing a player from the pool, required a Supabase function to ensure the transaction was atomic, and to also enable row locking during the process. Atomic transactions were necessary: the entire app architecture, from the backend to the frontend, was built on the asumption a team always has five players, so ensuring half completed trades could never go through was a necessity. This was done via row locking, where interacting with any player through a trade would lock that player to the transaction, ensuring no other user could also execute a trade simultaneously.

    With the functions to handle trades completed, all client-side functions were simplified heavily. They just called those trade functions with the right paramaters. Other things, such as trade windows and the two trade per window cap were easy to implement. Trade windows were fixed dates stored in the database that were pulled when accessing the trade page, and the trade history column (which was populated by the same functions that confirmed trades) was used to count the number of trades per window.

    This feature was a necessity. Trading is a big part of the fun in fantasy leagues, and especially in a sport as volatile as Street Fighter 6, ever changing teams were an inevitability that had to be accommodated.

    ***

    That concludes the core pages of the app, broadly simplified of course. I want to take the time now to rapid fire some features that were particularly fun or particularly tricky to implement.

    Sounds: While aesthetics and colours and button sizes are all important to good user experience, feedback is more important. Adding sounds to button presses, page switches, and all other interactables went a long way to breathe some needed life into the app.

    Session Caching: Reducing the number of API calls the app made was a constant battle. A Session class was created to cache data locally, from avatars to team data to trade windows. This was difficult to implement, with the main challenge being segmenting the API calls into different methods that were only called when necessary. There were plenty of issues with a single widget requiring data from an entirely different part of the API and vice-versa, which resulted in a lot of meticulous UX design to remove these widgets while simultaneously amending the rest of the page to ensure the removed widgets were not missed. One particularly tedious case came late into the league page's development: the draft picker showed all players, including claimed ones. But showing just unclaimed players would require an entire pull of the team_players table, and then filtering, to find unclaimed ones. The fix was actually the league feed: show the picks and make everyone watch, so when its their turn, they know who's taken!

    Help: Coming back down from the high which was the previous feature, a small help button was added to the header that changed based on the current page. Clicking it would give a brief explanation on the page's purpose and what can be done on it. It may sound unecessary, but as developers we tend to tunnel vision too hard, and overestimate how readable our products can be. A little help goes a long way!

    Credits and FAQs: On the settings page, which controlled the volume of sound effects, small widgets were added to show the version, credits, and FAQs. The credits button would open a small window highlighting the work of myself and good friend Nabil, who was effectively my Chief Creative Officer for this app, guiding all aesthetic decisions and making the app as beautiful as it is today. The FAQs would simply take you to the repo README file, which I amended to include a bunch of common questions I had from testers.

    ***

    To conclude, this project was not only incredibly fun to make, but a very important learning experience in what exactly it means to be a full-stack developer. I plan to continue work on the app, and if demand is high enough, perhaps port it to iOS or the web. Thank you for reading, and lastly, if you do have any other questions regarding the app, feel free to ask me, I'll take any chance possible to talk about the app!

    [1] Cambridge Advanced Learner's Dictionary & Thesaurus