Skip to contents

I like Ocarina of Time 100%. So let’s take a look at that.

Identify what you want

First up we need the game’s ID and category ID. That’s easy:

library(dplyr)
library(kableExtra)
library(speedrunr)

get_games("Ocarina of Time")
#> # A tibble: 8 × 9
#>   id       name_international           name_twitch name_abbr weblink released  
#>   <chr>    <chr>                        <chr>       <chr>     <chr>   <date>    
#> 1 j1l9qz1g The Legend of Zelda: Ocarin… "The Legen… oot       https:… 1998-11-21
#> 2 kdkjex1m The Legend of Zelda: Ocarin… "The Legen… ootmq     https:… 2002-01-01
#> 3 268vqkdp The Legend of Zelda: Ocarin… "The Legen… oot3d     https:… 2011-06-16
#> 4 76rkv4d8 Ocarina of Time Category Ex… "The Legen… ootextras https:… 1998-11-21
#> 5 m1zromd0 Ocarina of Time Beta Quest   "Ocarina o… ootbq     https:… 2015-11-14
#> 6 v1pol9m6 SM64: Ocarina of Time        "SM64: Oca… sm64oot   https:… 2018-03-26
#> 7 lde38o56 Ocarina of Time in Minecraft "Ocarina o… mcm_oot   https:… 2020-11-28
#> 8 v1pxwwz6 Zelda II: Ocarina of Time    ""          zelda_ii… https:… 2022-05-13
#> # ℹ 3 more variables: released_year <int>, romhack <lgl>, created <dttm>
get_categories("j1l9qz1g")
#> # A tibble: 12 × 6
#>    id       name          link                         type  miscellaneous rules
#>    <chr>    <chr>         <chr>                        <chr> <lgl>         <chr>
#>  1 q255jw2o 100%          https://www.speedrun.com/oo… per-… FALSE         ""   
#>  2 824qn3k5 100%          https://www.speedrun.com/oot per-… FALSE         "# I…
#>  3 zdnoz72q All Dungeons  https://www.speedrun.com/oo… per-… FALSE         ""   
#>  4 q25g198d Any%          https://www.speedrun.com/oo… per-… FALSE         "# *…
#>  5 02qe4z2y Any%          https://www.speedrun.com/oot per-… FALSE         "# I…
#>  6 z275w5k0 Defeat Ganon  https://www.speedrun.com/oo… per-… FALSE         ""   
#>  7 zd35jnkn Glitchless    https://www.speedrun.com/oo… per-… FALSE         ""   
#>  8 ndxlw1dq Glitchless    https://www.speedrun.com/oot per-… FALSE         "# I…
#>  9 9d85yqdn GSR           https://www.speedrun.com/oo… per-… FALSE         "# *…
#> 10 jdrwr0k6 MST           https://www.speedrun.com/oo… per-… FALSE         "# *…
#> 11 xd1wj828 No Wrong Warp https://www.speedrun.com/oo… per-… FALSE         ""   
#> 12 rklm8qdn Restricted    https://www.speedrun.com/oot per-… FALSE         "# I…

So we’re good.

game <- "j1l9qz1g"
category <- "q255jw2o"

oot100 <- get_runs(game = "j1l9qz1g", category = "q255jw2o", max = Inf)

Note that I’m useing max = Inf to make sure I get all the runs.

str(oot100)
#> tibble [967 × 22] (S3: tbl_df/tbl/data.frame)
#>  $ id             : chr [1:967] "z195w6wm" "y8w1qk1y" "yjo4jvoz" "y9jog0nm" ...
#>  $ weblink        : chr [1:967] "https://www.speedrun.com/oot/run/z195w6wm" "https://www.speedrun.com/oot/run/y8w1qk1y" "https://www.speedrun.com/oot/run/yjo4jvoz" "https://www.speedrun.com/oot/run/y9jog0nm" ...
#>  $ game           : chr [1:967] "j1l9qz1g" "j1l9qz1g" "j1l9qz1g" "j1l9qz1g" ...
#>  $ level          : logi [1:967] NA NA NA NA NA NA ...
#>  $ category       : chr [1:967] "q255jw2o" "q255jw2o" "q255jw2o" "q255jw2o" ...
#>  $ videos         : chr [1:967] "https://www.twitch.tv/videos/2089115687" "https://www.youtube.com/watch?v=75xbUuc3Q0U" "https://www.twitch.tv/videos/2071757626" "https://www.youtube.com/watch?v=TAsaFzVxeE4" ...
#>  $ status         : chr [1:967] "verified" "verified" "verified" "verified" ...
#>  $ comment        : chr [1:967] "[Retimed to 4:58:16 -LG]" "4th try dampe" "[Retimed to 5:14:29 -LG]" NA ...
#>  $ player_id      : chr [1:967] "jn95rndx" "v8l62g78" "jn95rndx" "8g516wyj" ...
#>  $ player_url     : chr [1:967] "https://www.speedrun.com/user/vaho_66" "https://www.speedrun.com/user/EricDaCleric" "https://www.speedrun.com/user/vaho_66" "https://www.speedrun.com/user/EdenSugar" ...
#>  $ player_name    : chr [1:967] "vaho_66" "EricDaCleric" "vaho_66" "EdenSugar" ...
#>  $ player_role    : chr [1:967] "user" "user" "user" "user" ...
#>  $ player_signup  : POSIXct[1:967], format: "2021-09-12 18:29:50" "2017-07-13 02:45:16" ...
#>  $ date           : Date[1:967], format: "2024-03-12" "2024-02-27" ...
#>  $ submitted      : POSIXct[1:967], format: "2024-03-13 00:15:15" "2024-03-01 07:47:04" ...
#>  $ time_primary   : num [1:967] 17896 13858 18869 15484 13539 ...
#>  $ time_realtime  : num [1:967] 17896 13858 18869 15484 13539 ...
#>  $ time_ingame    : int [1:967] 0 0 0 0 0 0 0 0 0 0 ...
#>  $ time_hms       : 'hms' num [1:967] 04:58:16 03:50:58 05:14:29 04:18:04 ...
#>   ..- attr(*, "units")= chr "secs"
#>  $ system_platform: chr [1:967] "nzelreqp" "nzelreqp" "nzelreqp" "nzelreqp" ...
#>  $ system_emulated: logi [1:967] FALSE FALSE FALSE FALSE FALSE FALSE ...
#>  $ system_region  : chr [1:967] "o316x197" "o316x197" "o316x197" "o316x197" ...

We want some additional data:

oot100 <- oot100 %>%
  add_platforms() %>%
  add_regions() %>%
  find_records()

oot100 %>% 
  arrange(time_hms) %>%
  select(time_hms, record, player_name, date, system_platform, system_region) %>%
  head(10) %>%
  kable() %>%
  kable_styling()
time_hms record player_name date system_platform system_region
03:00:39 TRUE zfg 2021-03-11 Wii Virtual Console JPN / NTSC
03:03:43 TRUE zfg 2021-02-22 Wii Virtual Console JPN / NTSC
03:04:39 TRUE zfg 2021-02-15 Wii Virtual Console JPN / NTSC
03:05:03 TRUE zfg 2021-02-10 Wii Virtual Console JPN / NTSC
03:05:49 TRUE zfg 2021-02-02 Wii Virtual Console JPN / NTSC
03:06:32 FALSE glitchymon 2021-02-26 Wii Virtual Console JPN / NTSC
03:08:33 TRUE zfg 2021-01-25 Wii Virtual Console JPN / NTSC
03:08:38 TRUE zfg 2021-01-20 Wii Virtual Console JPN / NTSC
03:09:19 TRUE zfg 2021-01-15 Wii Virtual Console JPN / NTSC
03:09:39 TRUE zfg 2021-01-05 Wii Virtual Console JPN / NTSC

Category Overview

Now we can take a look at the categories (recent) history, with highlighted records:

library(ggplot2)
library(ggrepel)
library(hrbrthemes)
library(hms)

oot100 %>%
  filter(time_hms < hms::hms(hours = 5)) %>%
  {
    ggplot(., aes(date, time_hms)) +
      geom_point(size = 1, alpha = .75) +
      geom_point(size = 2, data = filter(., record), aes(color = player_name)) +
      geom_label_repel(data = filter(., record),
                       aes(label = time_hms, color = player_name),
                       fill = "white", show.legend = F) +
      scale_x_date(date_breaks = "6 months", date_labels = "%b '%y") +
      scale_y_time(breaks = seq(2 * 60^2, 20 * 60^2,  1/3 * 60^2),
                   minor_breaks = seq(2 * 60^2, 20 * 60^2, 5 * 60)) +
      scale_color_brewer(palette = "Dark2") +
      labs(title = "Ocarina of Time: 100% Speedrun Record History",
           subtitle = paste0("All data from speedrun.com (n = ", nrow(.), ")"),
           x = "Date of Run", y = "Time",
           color = "Runner", caption = "Data limited to sub 5h runs") +
      theme_ipsum() +
      theme(legend.position = "top")
  }
#> Warning: ggrepel: 46 unlabeled data points (too many overlaps). Consider
#> increasing max.overlaps

Please note that the data on speedrun.com does not cover the whole history. There are lots of older runs missing here, and unless the mods come together und do some historic backlogging, that’s as good as it gets for now.

We can also take a look at the recent developments:

library(lubridate)

tmp <- oot100 %>%
  filter(time_hms < hms::hms(hours = 4, minutes = 30),
         date >= today() - months(6)) %>%
  select(player_name, time_hms, date)

bind_rows(
  tmp,
  tmp %>%
    group_by(player_name) %>%
    summarize(time_hms = min(time_hms), date = today()) %>%
    ungroup
) %>%
  {
    ggplot(., aes(x = date, y = time_hms, 
                  color = player_name, fill = player_name)) +
      geom_point(size = 1, alpha = .75) +
      geom_step() +
      geom_label_repel(
        data = . %>% 
          group_by(player_name) %>%
          summarize(y = min(time_hms), x = max(date)),
        aes(label = player_name, x = x, y = y),
        color = "black", alpha = .75, show.legend = F,
        hjust = 1, direction = "y", nudge_x = 60^2
      ) +
      scale_x_date(date_breaks = "1 month", date_labels = "%b '%y",
                   limits = c(as.Date(NA), today() + days(45))) +
      scale_y_time(breaks = seq(0, 20 * 60^2,  1/12 * 60^2),
                   minor_breaks = seq(0, 20 * 60^2, 1/24 * 60)) +
      scale_color_viridis_d(
        direction = -1, guide = FALSE, aesthetics = c("color", "fill")
      ) +
      labs(title = "Ocarina of Time: 100% Speedruns",
           subtitle = "All runs of the past 6 months",
           caption = "Data from speedrun.com",
           x = "Date of Run", y = "Time (H:M:S)") +
      theme_ipsum(grid = "X") + theme(axis.text.x = element_text(hjust = 0))
  }
#> Warning: The `guide` argument in `scale_*()` cannot be `FALSE`. This was deprecated in
#> ggplot2 3.3.4.
#>  Please use "none" instead.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> Warning: Removed 5 rows containing missing values or values outside the scale range
#> (`geom_label_repel()`).