SQL

Super Smash Brothers Player Statistics

Summary

  • This is a SQL project focused on measuring and improving my performance in the fighting game Super Smash Brothers Melee.

  • Queries used:

    WITH... AS (AKA CTEs)
    WHERE... (AND...)
    HAVING
    ROUND
    SUM
    COUNT
    GROUP BY
    ORDER BY
    CASE... (WHEN... THEN... END)
    CAST
    LEFT JOIN
    STRFTIME
    DATE
    TIME
    BETWEEN

Table of Contents

Section I. Research Questions

Super Smash Bros. Melee (hereon, “Melee”) is a fighting game for the Nintendo GameCube, which was released on November 21st, 2001. More than two decades later, people are still playing Melee. More specifically, the game’s competitive community is thriving despite the game’s age. How could such an old game still have such an active fan base? Slippi. Slippi is an ongoing modding project by members of the Melee community which aims to render several major quality-of-life additions to the game. The two most major changes were rollback netcode and an integrated online matchmaking system. I started playing Melee in 2023 after hearing about this modding project and have been playing since.

In addition to these two features, another interesting addition the Slippi team made was an automatic replay saving feature, which saves a replay of every match of Melee you play online. The replay files are stored right onto the device running Slippi as a .slp file, which can be viewed through the Slippi launcher. What’s interesting about these files are that they are laden with data. By passing the replay files through a parsing program, we can view all sorts of data: Player characters and names, the winner of each game, the date and time of each game, the stage played on…

The goal of this project is to kill two birds with one stone: get better at SQL and Melee. By parsing my replay files, and placing them into a database, I can practice writing SQL scripts that pull key insights about my gameplay out of my replay database. Once I have these key insights, I can determine my strengths and weaknesses and adapt my playstyle accordingly in-game.

It’s also important to mention that there are many different characters to play in Melee, but most players pick one character and stick to them. In my case, I’m interested in playing Falco.

Given that, these are the questions I wanted answered:

  1. How many games have I played on Falco?
    • Count of games on Falco.
  2. What are my best and worst matchups?
    • Average win-rate vs every character.
  3. What are my best and worst stages?
    • Average win-rate on every stage.
  4. What are my best and worst matchups on each stage?
    • Average win-rates on every matchup-stage combination.
  5. When do I play best?
    • Average win-rate on every day of the week.
    • Average win-rate at different times of day (e.g. Morning)
    • Average win-rate on every day of the week and at every time of day.

Section II. Data Collection

I used Slippi DB to parse replay files and place them into a SQLite database.

I’ve been accumulating replays over the course of approximately a year (since I started playing in 2023).

Total number of replays: 3313

Section III. SQL Queries

1. How many games have I played using “Falco”?

-- Starting by filtering down to only the games where I play Falco
WITH
falco_games AS (
    select
        me.character as bird 
    from players me, players op 
    where me.id != op.id -- x /= y (there are two unique objects in the domain)
    and me.game_id = op.game_id -- Gxy & Gyx (One object is in the same game as the other, and vice versa)
    and me.character='FALCO' -- Cx'Falco'  (One object has the property of playing Falco)
    and me.code='FROG#671' -- Ox'FROG#671' (That object also has my specific code)
) 
select bird as Character, count(*) as "Games played" -- Counting Games
FROM falco_games;

Output:

Character Games played
FALCO 2332

2. What are my best and worst matchups?

WITH
falco_games AS (
    select me.id, 
        me.game_id, 
        op.character as matchups, 
        me.winner as didIwin
    from players me, players op 
    where me.id != op.id
    and me.game_id = op.game_id
    and me.character='FALCO'
    and me.code='FROG#671'
)
select matchups as 'Matchups',
    round((sum(didIwin)*1.0 /count(*)*100), 1) as Winrate, 
    -- Winrate: The percetentage of time I win...
    count(*) as 'Games played'
FROM falco_games
GROUP BY matchups                                           -- ... each matchup
ORDER BY Winrate desc;

Output, ordered by Winrate:

Matchups Winrate Games played
MEWTWO 66.7 9
GAME_AND_WATCH 66.7 12
BOWSER 66.7 3
YOUNG_LINK 63.6 11
ROY 60 15
PIKACHU 57.1 14
SAMUS 56.7 30
CAPTAIN_FALCON 54.7 338
MARIO 52.9 34
DONKEY_KONG 52.9 51
SHEIK 52.8 159
FOX 51.3 392
FALCO 48.7 622
YOSHI 46.2 13
NESS 45.5 11
LUIGI 45.5 44
MARTH 44.9 365
KIRBY 42.9 7
ZELDA 40 5
GANONDORF 34.2 76
LINK 33.3 21
ICE_CLIMBERS 33.3 6
PEACH 31.3 48
JIGGLYPUFF 27.3 33
DR_MARIO 25 12
PICHU 0 1

3. What are my best and worst stages?

WITH
falco_games AS (
    select me.id, 
        me.game_id as gid, 
        me.winner as winner
    from players me, players op 
    where me.id != op.id
    and me.game_id = op.game_id
    and me.character='FALCO'
    and me.code='FROG#671'
)
select stage as Stage,
   round((sum(winner)*1.0/count(*)*100),1) as Winrate,
    count(*)
From falco_games
LEFT JOIN games on gid = games.id --Left joining with games to get stage data
GROUP BY Stage
HAVING count(*) > 10 -- Ignore games on stages I've played less than 10 times
ORDER BY Winrate desc;

Output, ordered by Winrate:

Stage Winrate count(*)
FINAL_DESTINATION 52.6 382
POKEMON_STADIUM 52.4 393
FOUNTAIN_OF_DREAMS 47.7 392
YOSHIS_STORY 46.8 389
DREAM_LAND_N64 46.6 371
BATTLEFIELD 46.1 399

4. What are my best and worst matchups on each stage?

WITH
falco_games AS (
    select me.id as mid, 
        me.game_id as gid,  
        op.character as matchup, 
        me.winner as winner
    from players me, players op 
    where me.id != op.id
    and me.game_id = op.game_id
    and me.character='FALCO'
    and me.code='FROG#671'
)
Select stage as Stage, matchup,
    round((sum(winner)*1.0/count(*)*100), 1) as Winrate,
    count(*)
From falco_games
LEFT JOIN games on gid = games.id
GROUP BY Stage, matchup
HAVING count(*) >= 10 -- filtering out stage-matchup combinations that I have played on/against less than 10 times
ORDER BY Winrate desc;

Output, ordered by Winrate:

Stage matchup Winrate count(*)
YOSHIS_STORY DONKEY_KONG 70 10
FINAL_DESTINATION SHEIK 62.5 32
BATTLEFIELD CAPTAIN_FALCON 61.3 62
POKEMON_STADIUM FOX 61.3 62
FOUNTAIN_OF_DREAMS CAPTAIN_FALCON 60.3 58
POKEMON_STADIUM LUIGI 60 10
BATTLEFIELD SHEIK 57.1 28
FOUNTAIN_OF_DREAMS SHEIK 57.1 21
FINAL_DESTINATION MARTH 56.3 64
FINAL_DESTINATION FOX 55.1 69
YOSHIS_STORY CAPTAIN_FALCON 54.4 57
FINAL_DESTINATION FALCO 53.8 93
DREAM_LAND_N64 CAPTAIN_FALCON 53.7 54
POKEMON_STADIUM FALCO 53.7 108
POKEMON_STADIUM SHEIK 51.9 27
FINAL_DESTINATION GANONDORF 50 14
FOUNTAIN_OF_DREAMS MARTH 50 54
POKEMON_STADIUM CAPTAIN_FALCON 50 52
YOSHIS_STORY FOX 48.5 66
DREAM_LAND_N64 FOX 48.1 54
DREAM_LAND_N64 FALCO 47.4 97
FINAL_DESTINATION CAPTAIN_FALCON 47.3 55
BATTLEFIELD FALCO 47.1 102
FOUNTAIN_OF_DREAMS FOX 47 66
BATTLEFIELD FOX 46.6 73
YOSHIS_STORY FALCO 45.7 105
BATTLEFIELD PEACH 45.5 11
FOUNTAIN_OF_DREAMS FALCO 45.3 117
DREAM_LAND_N64 SHEIK 44.8 29
DREAM_LAND_N64 GANONDORF 43.8 16
POKEMON_STADIUM MARTH 43.5 69
YOSHIS_STORY MARTH 41.7 60
YOSHIS_STORY SHEIK 40.9 22
DREAM_LAND_N64 MARTH 39.7 58
BATTLEFIELD MARTH 39 59
FOUNTAIN_OF_DREAMS GANONDORF 36.4 11
DREAM_LAND_N64 DONKEY_KONG 30 10
YOSHIS_STORY GANONDORF 28.6 14
POKEMON_STADIUM GANONDORF 27.3 11
BATTLEFIELD GANONDORF 10 10

5. When do I play best?

5.1. Day of the Week

WITH
falco_games AS (
    select me.id as mid, 
        me.game_id as gid, 
        me.winner as winner
    from players me, players op 
    where me.id != op.id
    and me.game_id = op.game_id
    and me.character='FALCO'
    and me.code='FROG#671'
)
select
CASE CAST (strftime('%w', DATE(start_time)) as int)   --strftime(%w,...) to get the day of the week based on the date of the game
    WHEN 0 then 'Sunday'-- Instead of expressing each day of week as an integer (sunday=0... saturday=6), strings
    WHEN 1 then 'Monday'
    WHEN 2 then 'Tuesday'
    WHEN 3 then 'Wednesday'
    WHEN 4 then 'Thursday'
    WHEN 5 then 'Friday'
    else 'Saturday' end as "Day of Week",
    round((sum(winner)*1.0/count(*)*100), 1) as Winrate,
    count(*) as Games
From falco_games
LEFT JOIN games on gid = games.id
GROUP BY 1
ORDER BY Winrate desc;

Output, ordered by Winrate:

Day of Week Winrate Games
Wednesday 52.3 369
Thursday 50.9 379
Saturday 48.1 318
Tuesday 47.5 396
Monday 47.4 388
Friday 47.2 299
Sunday 46.4 183

5.2. Time of Day

WITH
falco_games AS (
    select me.id as mid, 
        me.game_id as gid, 
        me.winner as winner
    from players me, players op 
    where me.id != op.id
    and me.game_id = op.game_id
    and me.character='FALCO'
    and me.code='FROG#671'
)
select 
    CASE -- Defining different times of the day based on the hour of the day
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 0 AND 5 THEN 'Late Night (12AM-5AM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 5 AND 9 THEN 'Morning (5AM-9AM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 9 AND 14 THEN 'Noon (9AM-2PM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 14 AND 19 THEN 'Afternoon (2PM-7PM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) >= 19 THEN 'Evening (7PM-12AM)'
    END AS "Time of Day",
    round((sum(winner)*1.0/count(*)*100), 1) as Winrate,
    count(*) as "Games Played"
From falco_games
LEFT JOIN games on gid = games.id
GROUP BY 1
ORDER BY CASE -- Aesthetic reordering so 'Morning' comes first, and 'Late Night' comes last
    WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 0 AND 5 THEN 5
    WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 5 AND 9 THEN 1
    WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 9 AND 14 THEN 2
    WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 14 AND 19 THEN 3
    WHEN CAST (strftime('%H', TIME(start_time)) as int) >= 19 THEN 4
END;

Output, ordered by Winrate:

Time of Day Winrate Games Played
Morning (5AM-9AM) 47.8 370
Noon (9AM-2PM) 61.5 13
Afternoon (2PM-7PM) 48.6 210
Evening (7PM-12AM) 46.2 650
Late Night (12AM-5AM) 50.5 1089

5.3. Day of the Week and Time of Day

WITH
falco_games AS (
    select me.id as mid, 
        me.game_id as gid, 
        me.winner as winner
    from players me, players op 
    where me.id != op.id
    and me.game_id = op.game_id
    and me.character='FALCO'
    and me.code='FROG#671'
)
select
    CASE CAST (strftime('%w', DATE(start_time)) as int) 
        WHEN 0 then 'Sunday'
        WHEN 1 then 'Monday'
        WHEN 2  then 'Tuesday'
        WHEN 3 then 'Wednesday'
        WHEN 4 then 'Thursday'
        WHEN 5 then 'Friday'
        else 'Saturday' 
    END as 'Day of Week',
    CASE 
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 0 AND 5 THEN 'Late Night (12AM-5AM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 5 AND 9 THEN 'Morning (5AM-9AM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 9 AND 14 THEN 'Noon (9AM-2PM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) BETWEEN 14 AND 19 THEN 'Afternoon (2PM-7PM)'
        WHEN CAST (strftime('%H', TIME(start_time)) as int) >= 19 THEN 'Evening (7PM-12AM)'
    END AS 'Time of Day',
    round((sum(winner)*1.0/count(*)*100), 1) as Winrate,
    count(*) as Games
From falco_games
LEFT JOIN games on gid = games.id
GROUP BY 1, 2
HAVING Games >= 10
ORDER BY Winrate desc;

Output, ordered by Winrate:

Day of Week Time of Day Winrate Games
Thursday Late Night (12AM-5AM) 56 184
Wednesday Afternoon (2PM-7PM) 56 75
Wednesday Evening (7PM-12AM) 55.1 78
Monday Morning (5AM-9AM) 54.5 33
Sunday Afternoon (2PM-7PM) 54.5 11
Tuesday Morning (5AM-9AM) 53.3 15
Tuesday Afternoon (2PM-7PM) 53.1 32
Saturday Late Night (12AM-5AM) 52.4 187
Wednesday Late Night (12AM-5AM) 52.4 126
Monday Late Night (12AM-5AM) 50 184
Saturday Morning (5AM-9AM) 50 32
Wednesday Noon (9AM-2PM) 50 10
Sunday Morning (5AM-9AM) 49.4 89
Friday Morning (5AM-9AM) 48.8 43
Friday Afternoon (2PM-7PM) 47.5 40
Tuesday Late Night (12AM-5AM) 47.1 261
Friday Late Night (12AM-5AM) 46.9 98
Friday Evening (7PM-12AM) 46.6 118
Wednesday Morning (5AM-9AM) 46.3 80
Thursday Evening (7PM-12AM) 46.2 106
Tuesday Evening (7PM-12AM) 45.5 88
Sunday Late Night (12AM-5AM) 44.9 49
Saturday Evening (7PM-12AM) 44.7 76
Monday Evening (7PM-12AM) 44 150
Thursday Morning (5AM-9AM) 42.3 78
Sunday Evening (7PM-12AM) 38.2 34
Monday Afternoon (2PM-7PM) 38.1 21
Saturday Afternoon (2PM-7PM) 21.7 23

Section IV. Interpretation and Key Insights

Key Insights

  • I need practice against Jigglypuff.
  • I need practice on Battlefield.
  • Counterpick to Final Destination against Sheik, Marth and Fox.
  • Avoid Battlefield against Marth, Peach, Fox and Falco.
  • Against Captain Falcon, counterpick to Battlefield and avoid Final Destination. ### Matchups First, looking at my matchup win-rates, we can see that my best matchups are against Mewtwo, Bowser, and Game and Watch. This makes sense because these characters are generally considered weak characters according to the community tier-list (i.e., a “low-tier” character), which also explains why I’ve played against so few of them. Compare this to someone like Captain Falcon, who I’ve played against many more times (n=338). My matchups against high-tiers generally matter more than against non-high-tiers, simply because more people play them.

My highest win-rate against a “high-tier” character is 54.7% against Captain Falcon, and my lowest is 27.3% against Jigglypuff. This suggests that I need more practice against Jigglypuff, and that I’m well practiced against Captain Falcon.

Stages

Looking at my stage win-rates, it’s interesting to see that Final Destination is my best stage. Traditional wisdom suggests Falco, my character, is bad on Final Destination. This is bizarre, but situationally advantageous when counter-picking a stage in tournament. 1 Looking at my stage-matchup win-rates, I am more than 50% likely to beat Sheik, Marth and Fox on Final Destination, who all generally like to counterpick to Final Destination. However, my win-rate against Captain Falcon is losing on Final Destination at 47.3%. So, if I beat Sheik, Marth or Fox in game 1, they might unknowingly counterpick my best stage. If I lose in game 1, I know to counterpick to Final Destination, unless I’m playing against Captain Falcon.

Furthermore, Battlefield is my worst stage, which according to traditional wisdom should be one of Falco’s best stages. Looking at my stage-matchup win-rates, I can see that I am more likely to lose on Battlefield against Marth, Peach, Fox, and Falco. So, I should avoid Battlefield against Marth, Peach, Fox and Falco whenever possible. But funnily enough, Captain Falcon is again the exception. My win-rate against Captain Falcon on Battlefield is my third best mathcup at 61.3%. So, I should pick Battlefield against Captain Falcon.

Day of Week/Time of Day

I was very surprised to see that I play the best on Wednesdays. This was also reflected in the Day of the Week - Time of Day win-rates, where my top 3 performances were on varying times on Wednesday. 2 I can only speculate, but I suspect this outcome is a result of my and other people’s work schedules. For around a year Wednesdays were my day off, which gave me more time to relax and play Melee. Generally, people don’t have Wednesdays off, so my opponents were probably suffering from the stress of work on Wednesdays. I believe it was a combination of my atypical and other people’s typical work schedule. This simply suggests that stress makes people perform worse in Melee.

Footnotes

  1. Tournament sets are usually best out of 3. Rock, Paper, Scissors determines who picks the stage for the first game. The loser of the first game gets to pick the stage for the second game. The strategy of picking a stage that favors your character over the opponent’s is called counter-picking.↩︎

  2. Thursday at 12-5AM is my highest winrate day-hour, which is technically a calendar-Thursday. However, it’s better to interpret it as a Wednesday play session.↩︎