If you haven't already checked it out, go watch CGPGrey's Rock-Paper-Scissors YouTube Game.
https://www.youtube.com/watch?v=PmWQmZXYd74
In this post, I'm going to explore what all the possible paths available are. Let's import some packages first.
using PyCallusing Condausing Graphsusing WGLMakieusing CairoMakieusing GraphMakieusing GraphMakie.NetworkLayoutusing JSON3using JSServe, Markdownusing ColorSchemesPage(exportable=true, offline=true)CairoMakie.activate!()Makie.inline!(true)Fortunately for us, CGPGrey was kind enough to put links to the choices in the description of (almost) every video. We can use Google's YouTube API to get the video descriptions and get all the YouTube links in the description.
We are going to use the google-api-python-client in Python from Julia.
Code
API_KEY = ENV["YOUTUBE_API_KEY"]; # Get API_KEY from google consolebuild = pyimport("googleapiclient.discovery").build # from googleapiclient.discovery import buildyoutube = build("youtube", "v3", developerKey=API_KEY) # call build function in PythonNow we can get the description of every video, extract the metadata from
it into a Dict of Dicts, build a graph:
Code
youtubeid(url) = string(first(split(replace(url, "https://www.youtube.com/watch?v=" => ""), "&t")))
function metadata(url) id = youtubeid(url) request = youtube.videos().list(part=["snippet", "statistics"], id=id) response = request.execute() description = response["items"][1]["snippet"]["description"] title = response["items"][1]["snippet"]["title"] views = parse(Int, response["items"][1]["statistics"]["viewCount"]) if url == "https://www.youtube.com/watch?v=CPb168NUwGc" # Special case for https://www.youtube.com/watch?v=CPb168NUwGc (description for this is not standard) return (; description="""WIN: https://www.youtube.com/watch?v=RVLUX6BUEJILOSE / DRAW: https://www.youtube.com/watch?v=jDQqv3zkbIQ
🌐 Website: https://www.cgpgrey.com💖 Patreon: https://www.patreon.com/cgpgrey📒 Cortex: http://www.cortexbrand.com
⛔️ Ah01F ✅""", title="🔴", views) end (; description, title, views)end
function links(url; visited=Dict(), duplicate_links=false) m = metadata(url) r = Dict( :id => youtubeid(url), :code => last(split(strip(m.description), "\n")), # last line is a special code :url => url, :links => [], :children => [], :title => m.title, :views => m.views, ) for line in split(m.description, "\n") if occursin("https://www.youtube.com/watch?v=", line) _status, video = split(line, ":", limit=2) video = strip(video) push!(r[:links], Dict(:status => string(_status), :url => string(video))) end end
for link in r[:links] url = link[:url] if !(url in keys(visited)) visited[url] = Dict() s = links(url; visited, duplicate_links) push!(r[:children], s) visited[url] = s else duplicate_links && push!(r[:children], visited[url]) end end return rend
function cached_links(url; duplicate_links) bfile = """$(youtubeid(url))-$(duplicate_links ? "dup-links" : "no-dup-links").json""" if isfile(bfile) return JSON3.read(bfile) end r = links(url; duplicate_links) open(bfile, "w") do f JSON3.write(f, r) end rend
function _clean_titles(str) t = join([c for c in str if isascii(c)]) t = strip(t) if occursin("Cortex", t) return "" end string(t)end
function _node_builder(nodes, d) for c in d[:children] push!(nodes, (; id=c[:id], title=_clean_titles(c[:code]), url=c[:url], views=c[:views])) _node_builder(nodes, c) endend
function _graph_builder(G, d, ids) from = d[:id] for c in d[:children] to = c[:id] add_edge!(G, findfirst(isequal(from), ids), findfirst(isequal(to), ids)) _graph_builder(G, c, ids) endend
function get_nodes(data) nodes = [(; id=data[:id], title=_clean_titles(data[:title]), url=data[:url], views=data[:views])] _node_builder(nodes, data) nodes = unique(nodes) ids = [n.id for n in nodes] titles = [n.title for n in nodes] urls = [n.url for n in nodes] (; ids, titles, urls, nodes)end
function grapher(data, ids) G = SimpleDiGraph(length(ids)) _graph_builder(G, data, ids) Genddata = cached_links("https://www.youtube.com/watch?v=PmWQmZXYd74", duplicate_links=true)(; ids, titles, urls, nodes) = get_nodes(data)G = grapher(data, ids){111, 206} directed simple Int64 graphThere's 111 videos in this graph with 206 connections between the videos.
Here's what that graph visualized looks like:
Code
set_theme!(; size=(1600, 900), fonts=(; title="CMU Serif"))views = [node[:views] for node in nodes]min_val, max_val = extrema(views[2:end])normed_views = (views .- min_val) ./ (max_val - min_val)colors = cgrad(:viridis, scale=log)node_colors = ColorSchemes.get.(Ref(colors), normed_views)
f, ax, p = graphplot(G; nlabels=titles, nlabels_fontsize=10, node_color=node_colors, node_size=20, arrow_size=8, layout=Stress(dim=3))Colorbar(f[1, 2], limits=extrema(views), colormap=colors, label="YouTube Views")# hidedecorations!(ax); hidespines!(ax);# offsets = [Point2f(0.1, -0.5) for _ in p[:node_pos][]]# offsets[1] = Point2f(0.1, 0.5)# p.nlabels_offset[] = offsets# autolimits!(ax)# ax.title = "CGP Grey's Rock-Paper-Scissors YouTube Game"f
This graph contains a lot of duplicate links to the same video. For example when losing after different number of wins, you might end up at the same video. Let's remove those connections so we can visualize it as a tree.
Code
data = cached_links("https://www.youtube.com/watch?v=PmWQmZXYd74", duplicate_links=false)(; ids, titles, urls, nodes) = get_nodes(data)G = grapher(data, ids){111, 110} directed simple Int64 graphThere's 111 videos in this graph with 110 connections between the videos.
Here's what the graph now visualized looks like:
Code
set_theme!(; size=(1600, 900), fonts=(; title="CMU Serif"))
views = [node[:views] for node in nodes]min_val, max_val = extrema(views[2:end])normed_views = (views .- min_val) ./ (max_val - min_val)colors = cgrad(:viridis, scale=log)node_colors = ColorSchemes.get.(Ref(colors), normed_views)
# If there's a space it is probably a unique namenode_size = [length(split(strip(t))) > 1 ? 25 : 15 for t in titles]
f, ax, p = graphplot(G; nlabels=titles, nlabels_fontsize=15, node_color=node_colors, node_size, arrow_size=8, arrow_shift=:end, layout=Buchheim())hidedecorations!(ax);hidespines!(ax);offsets = [Point2f(0.1, -1.5) for _ in p[:node_pos][]]offsets[1] = Point2f(0.1, 0.5)p.nlabels_offset[] = offsetsautolimits!(ax)ax.title = "CGP Grey's Rock-Paper-Scissors YouTube Game"Colorbar(f[1, 2], limits=extrema(views), colormap=colors, label="YouTube Views")f
There we have it; a flowchart of the Rock-Paper-Scissors game.
Here's a table that contains the sorted view count as of April 19th, 2024.
Code
import DataFrames as DF
# Create an empty DataFramedf = DF.DataFrame(Title=String[], Views=Int[])
for idx in sortperm(views, rev=true) push!(df, (; Title=titles[idx], Views=views[idx]))end
# Display the DataFramedisplay(df)| Title | Views | |
|---|---|---|
| 1 | One-in-a-Million YouTube Game: Can YOU Win? | 1073774 |
| 2 | R1h01W | 420943 |
| 3 | Really Over | 386865 |
| 4 | R2xF | 354432 |
| 5 | Post Game | 306757 |
| 6 | R2h01W | 299879 |
| 7 | 285998 | |
| 8 | R1h03W | 234144 |
| 9 | R1h04W | 228726 |
| 10 | Ah01F | 218414 |
| 11 | R3h01W | 195832 |
| 12 | R1h12W | 158979 |
| 13 | R1h05W | 148637 |
| 14 | R1h06W | 136197 |
| 15 | R1h07W | 127503 |
| 16 | R2h02W | 121754 |
| 17 | R1hXF | 121434 |
| 18 | R1h08 | 120358 |
| 19 | R1h09W | 117182 |
| 20 | R1h11W | 116414 |
| 21 | R1h10W | 115443 |
| 22 | Ah02F | 114839 |
| 23 | Billion or Bust | 111327 |
| 24 | R1h02F | 106864 |
| 25 | R1h13W One Million Rock | 106053 |
| 26 | R3h02W | 103728 |
| 27 | RTh14W | 99236 |
| 28 | RTh19W | 98928 |
| 29 | RTh15W | 92640 |
| 30 | RTh18W | 89046 |
If you liked this blog post, consider subscribing to CGP Grey's Patreon so that they can make more awesome content like this.
If you are interested in viewing all the videos, you can check them out below:
Code
using IJulia
function display_youtube_video(node) video_id = split(node.url, "=")[end] title = node.title if isempty(title) title = "---no special code---" end html_code = """<details> <summary>$(title)</summary> <iframe width="560" height="315" src="https://www.youtube.com/embed/$video_id" frameborder="0" allowfullscreen></iframe></details> """ display("text/html", HTML(html_code))end
@assert unique(nodes) == nodes
display_youtube_video.(nodes);