← Back to the gamebug or idea?Terrapin Trail//Code BibleIIhow the game works, archive to van
Reel 17 · 7 min read

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):

●runner.py : 292–318trace ↗
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)
play: everything the game needs, decided on the server, embedded in the page.

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.

●assets/web/play.js : 82–91trace ↗
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  });
The page keeps every taped show in the browser as a playlist of real recordings ('My tapes' in the browser).

On tt:runend the page offers to post the score, then shows the rank and a challenge link carrying the run's seed:

●assets/web/play.js : 96–103trace ↗
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  }
challengeUrl: same tour, same rules, same luck, tagged as a challenge.

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:

●assets/game/game.js : 3869–3874trace ↗
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 () {
window.TTGame: the only way in from outside.

The van stereo then fetches that night's songs and plays them in order:

●assets/web/play.js : 25–37trace ↗
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">&#9654; 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 &#8599;</a>' : "");
37    else if (p.track) np.innerHTML = "&#9835; " + esc(p.track.title) + " &middot; " + esc(p.show.date) + " " + esc(p.show.venue || "") +
TTMusic.play: load a recording's tracks and start the first; the tapes panel's buttons call it too.

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:

●assets/web/browser.js : 574–585trace ↗
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  }
openGame: the game replaces the middle column; phones and narrow windows go to /play instead.

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:

●assets/web/browser.js : 627–633trace ↗
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  }
One setting, two pages.

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:

●assets/game/game.js : 472–494trace ↗
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 = "(?:'|&#39;|&#x27;)";
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(\\?'|&#39;)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  }
bandify: a short list of swaps applied to the rendered HTML; Dead-family bands keep the originals.
Key takeaways
  • 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.