The page around the game
Boot globals in, events out, the TTGame handle, the van stereo, the in-page desktop game, shared colors, band-fitting jokes.
The game itself is still one file. Around it sits a thin page, play.html with play.js, that supplies the tour and rules, plays the music, and takes the finished run to the shared board. The game and the page talk through a few events and one small handle.
Handing the game its tour
/play builds the tagged tour, decodes the rules, issues a run ticket and writes all of it into the page as boot data. A short script turns that into the globals the game reads at load: __TT_GENERATED__ (the tour), __TT_DNA__ (the rules) and __TT_SEED__ (the luck):
292 def play() -> BaseResponse: 293 """The game. With ?tag= it plays that shared tour under the chosen rules and 294 hands out a run ticket so the score can go on the tour's public board.""" 295 base = request.args.get("tag") 296 boot: dict = {"donate": DONATE_URL} 297 if base: 298 try: 299 info, tour = build_tagged_tour(base) 300 except archive.ArchiveError: 301 return redirect("/") 302 values = dna.decode(request.args.get("dna") or "CLASSIC") 303 code = dna.encode(values) 304 luck = request.args.get("luck") 305 try: 306 luck_n = int(luck) if luck else None 307 except ValueError: 308 luck_n = None 309 base = tour["tag"] 310 run = trust.start_run(store, base, code, len(tour["stops"]), 1, luck_n) 311 if code not in dna.PRESETS and store.get("ruleset:" + code): 312 store.zincr("rulesets:plays", code) 313 boot.update({ 314 "tag": base, "fullTag": tags.full_tag(base, code), "dna": code, "rules": dna.label(code), 315 "values": values, "tour": tour, "run": run["run"], "seed": run["seed"], 316 "band": info["band"], "year": info["year"], 317 }) 318 return _page("play.html", boot)
Events out of the game
The game announces two moments with DOM events and otherwise knows nothing about the page: tt:taped when a show is taped, and tt:runend when a run ends.
82 window.addEventListener("tt:taped", function (ev) { 83 var t = ev.detail || {}; 84 if (!t.identifier) return; 85 try { 86 var tapes = JSON.parse(localStorage.getItem("tt_tapes") || "[]").filter(function (x) { return x.identifier !== t.identifier; }); 87 t.tag = B.fullTag || null; t.at = Date.now(); 88 tapes.unshift(t); 89 localStorage.setItem("tt_tapes", JSON.stringify(tapes.slice(0, 300))); 90 } catch (e) {} 91 });
On tt:runend the page offers to post the score, then shows the rank and a challenge link carrying the run's seed:
96 function challengeUrl(name, seed) { 97 var q = new URLSearchParams(); 98 if (B.dna && B.dna !== "CLASSIC") q.set("dna", B.dna); 99 q.set("luck", seed); 100 q.set("utm_source", "challenge"); q.set("utm_medium", "link"); q.set("utm_campaign", B.fullTag); 101 if (name) q.set("utm_content", name); 102 return location.origin + "/t/" + B.tag + "?" + q.toString(); 103 }
A handle into the game
The page needs two things from the game: where the van is, so "Tonight's show" plays the right night, and a way to change the color scheme live:
3869 window.TTGame = { 3870 currentStop: function () { return S && S.role && STOPS && typeof S.stop === "number" ? STOPS[S.stop] : null; }, 3871 // TT-THEME: the archive browser's color picker shares this setting. 3872 setTheme: function (t) { if (THEMES.indexOf(t) === -1) return; OPTS.theme = t; applyOpts(); }, 3873 // TT-BUG: a short, readable snapshot for bug reports. 3874 debugState: function () {
The van stereo then fetches that night's songs and plays them in order:
25 var TTMusic = { 26 play: function (identifier) { return player ? player.play(identifier) : Promise.resolve(false); }, 27 }; 28 window.TTMusic = TTMusic; 29 function renderStereo(p) { 30 var np = $("tt-np"); if (!np) return; 31 $("tt-pause").innerHTML = p.playing ? "⏸" : "▶"; 32 if (p.blocked) { 33 np.innerHTML = '<button type="button" id="tt-resume">▶ Keep listening</button> ' + esc(p.track ? p.track.title : ""); 34 $("tt-resume").onclick = function () { player.toggle(); }; 35 } else if (p.loading) np.textContent = "Threading the tape..."; 36 else if (p.error) np.innerHTML = esc(p.error) + (p.show ? ' <a href="' + esc(p.show.details) + '" target="_blank" rel="noopener">Open it on the archive ↗</a>' : ""); 37 else if (p.track) np.innerHTML = "♫ " + esc(p.track.title) + " · " + esc(p.show.date) + " " + esc(p.show.venue || "") +
Inside the browser page, and one color setting
On a desktop the archive browser opens the game in its middle column, in an iframe with embed=1. The game hides its own back link and tag, and reports posted scores and "close" to the page around it with postMessage:
574 function openGame(href) { 575 var url = new URL(href, location.origin); 576 url.searchParams.set("embed", "1"); 577 document.body.classList.add("gaming"); // the game's own stereo takes over the controls 578 $("gameFrame").src = url.pathname + url.search; 579 var pop = new URL(href, location.origin); pop.searchParams.delete("embed"); 580 $("gamePop").href = pop.pathname + pop.search; 581 $("gameTag").textContent = url.searchParams.get("tag") ? "#" + url.searchParams.get("tag") + (url.searchParams.get("dna") && url.searchParams.get("dna") !== "CLASSIC" ? url.searchParams.get("dna") : "") : "Classic: Grateful Dead 1970-79"; 582 $("browseArea").hidden = true; $("gamePanel").hidden = false; 583 window.scrollTo(0, 0); 584 $("gameFrame").focus(); 585 }
The color picker in the browser writes the game's own saved option (terrapin_opts.theme), so the choice is the game's choice too. A change made while a game is open reaches it through TTGame.setTheme, or through the browser's storage event in another tab:
627 function setTheme(t) { 628 var o = gameOpts(); o.theme = t; 629 try { localStorage.setItem("terrapin_opts", JSON.stringify(o)); } catch (e) {} 630 applyTheme(t); 631 var f = $("gameFrame"); 632 try { if (f.contentWindow && f.contentWindow.TTGame) f.contentWindow.TTGame.setTheme(t); } catch (e) {} 633 }
Jokes that fit the band
The game's humor was written about the Grateful Dead. For any band outside the Dead family, bandify rewrites the dozen lines that name the Dead (Jerry, Pigpen, stealies) as each screen is drawn:
472 function bandify(html) { 473 if (!TOUR_BAND || DEAD_FAMILY.test(TOUR_BAND)) return html; 474 if (!bandSwaps) { 475 var b = esc(TOUR_BAND), B = b.toUpperCase(), q = "(?:'|'|')"; 476 bandSwaps = [ 477 [/HONK FOR JERRY/g, "HONK FOR " + B], 478 [/honk for Jerry/g, "honk for " + b], 479 [new RegExp("the best " + q + "72 " + q + "Dark Star\\." + q, "g"), "the best version of " + b + "'s longest jam."], 480 [/On tour since Pigpen\./g, "On tour since the band's first gig."], 481 [/Jerry would not approve\./g, b + " would not approve."], 482 [/unlicensed stealie shirts/g, "unlicensed " + b + " shirts"], 483 [/not your stealie monkeys/g, "not your monkeys"], 484 [/Steal Your Face on the tailgate/g, b + " stickers on the tailgate"], 485 [/Very un-Dead of you\./g, "Very off-brand for a " + b + " fan."], 486 [/The Deadicated Grandma/g, "The Dedicated Grandma"], 487 [/following the Grateful Dead(\\?'|')s real tours across the decade[^.]*\./g, "following " + b + " on real tours built from the Live Music Archive."], 488 [/Every year is a real Grateful Dead run[^<]*/g, "Every tour here is a real " + b + " run, built from the Live Music Archive."] 489 ]; 490 if (/^phish$/i.test(TOUR_BAND)) bandSwaps.push([/What are you, a Phish fan\?/g, "What are you, a Deadhead?"]); 491 } 492 for (var i = 0; i < bandSwaps.length; i++) html = html.replace(bandSwaps[i][0], bandSwaps[i][1]); 493 return html; 494 }
- The page supplies tour, rules and luck as globals before game.js loads.
- The game reports out through tt:taped and tt:runend, and is reached only through window.TTGame.
- On desktop the game runs inside the browser page; scores and closing go back via postMessage.
- One saved color setting drives both pages; band-specific jokes are swapped at render time.