Street Fighter 6 Matchup Lab¶
TL;DR
Street Fighter 6 is a competitive fighting game with a global ranked ladder. This tool scrapes your match history, stores it in PostgreSQL, runs analytics in Python, and serves the results as a fast, shareable static report. If you've built recurring KPI dashboards or automated reporting pipelines at work, this is the same architecture — different domain.
Outside of my family and my work, I try to get some games in a few times per week after the kids are asleep. Lately that game has been Street Fighter VI.
This page is my attempt at a “training partner” that doesn’t forget: it turns match history into a quick, shareable snapshot of how you’re actually performing—trends, matchups, and one clear place to focus next.
I’ll keep evolving the analytics under the hood, but the goal stays the same: highlight how a little data can create real leverage when the competition is tight.
Pull up your own stats and see where to focus.
How it works¶
This project is a simple pipeline: scrape match history on a schedule, store it, generate deterministic reports offline, then let the browser render the results instantly.
The left diagram shows the system architecture (what talks to what). The right diagram shows the lifecycle over time (what happens each run, and what the user does when they visit the page).
graph TB
A["Buckler<br/>Headless Browser"]
B[("PostgreSQL<br/>Match History")]
C["Python<br/>Report Generator"]
D[/"Static JSON<br/>Reports"/]
E["Browser<br/>Plotly Charts"]
A -->|Scrape CFN| B
B -->|SQL queries| C
C -->|Offline transforms| D
D -->|On load| E
stateDiagram-v2
[*] --> Scraping : scheduled run
Scraping --> Stored : write match history
Stored --> Generating : build reports
Generating --> Published : emit JSON
Published --> Viewing : user loads page
Viewing --> Published : reload
Published --> Scraping : next run
Under the Hood¶
The core decision was to keep complexity out of the browser. Reports generate offline — Python pulls from PostgreSQL, runs the matchup analytics, and emits static JSON. Page load is instant because there’s nothing to compute at request time, and every calculation lives in auditable source code.
Match history is filtered to ranked play only. Casual matches introduce noise that skews matchup numbers — ranked data is noisier moment-to-moment but honest about actual performance trends. The scraper hits Buckler’s Boot Camp on a schedule, so when you load a report the timestamp tells you exactly how fresh it is.
The next phase is opponent-level pattern recognition — not just which characters you lose to, but why the data points to specific habits or gaps to work on.
Stack: Python · PostgreSQL 16 · Plotly · MkDocs Material · GitHub Actions