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

The server

One Flask app for pages, the archive API, shared boards and this guide; shared state behind a small store interface.

The original game had one optional serverless function for its leaderboard. The Infinity edition has a real server: a single Flask app that serves the pages, talks to the Internet Archive, keeps the shared leaderboards, and mounts this guide. The same app runs in the workspace and on Vercel; only where it keeps shared state changes.

One app, every route

create_app builds everything. It takes the cache directory and a store as parameters, so a test can boot the whole app over a temp directory and a fresh store, offline:

●runner.py : 172–185trace ↗
172def create_app(data_dir: Path, store=None) -> Flask:
173    """Build the Flask app, reading and writing its caches under ``data_dir``.
174
175    ``data_dir`` is a parameter (not a hardcoded path) so a test can point a
176    throwaway instance at a temp directory seeded with cache fixtures and drive
177    the whole flow offline, exactly as the live app runs against ``DATA_DIR``.
178    ``store`` holds the shared state (leaderboards, tags, run tickets); it
179    defaults to Redis when configured, else files under ``data_dir``.
180    """
181    app = Flask("terrapin_trail", static_folder=None)
182    store = store if store is not None else make_store(data_dir)
183
184    # ------------------------------------------------------------ helpers
185    def band_name_for(collection: str) -> str:
create_app: the archive caches live under data_dir; shared state goes through store (Redis when configured, JSON files otherwise).

The routes fall into four groups: pages (/, /t/<tag>, /play, /guide/...), the archive browser API (/api/bands, /api/years, /api/tours, /api/tour/<tag>, /api/show/<id>), rules (/api/dna, /api/rulesets), and boards (/api/score, /api/board/<tag>, /api/daily). The original paste-a-link flow (/api/band) and the game's local Hall signal (/api/hall, always 503) remain.

Shared state behind one small interface

Everything that must be shared between players -- tags, run tickets, boards, share counts, the rule gallery, Tour of the Day -- goes through store.py. Two classes implement the same dozen operations; make_store picks one:

●store.py : 231–235trace ↗
231def make_store(data_dir: Path):
232    url = redis_url_from_env()
233    if url:
234        return RedisStore(url)
235    return FileStore(data_dir / "store")
make_store: a Redis URL in the environment means the Vercel deploy; otherwise a JSON-file store for the single workspace process.
●store.py : 31–39trace ↗
31def redis_url_from_env() -> str | None:
32    for name in _REDIS_ENV_NAMES:
33        if os.environ.get(name):
34            return os.environ[name]
35    # Vercel's Redis integration prefixes the variable with the store's name.
36    for name, value in os.environ.items():
37        if name.endswith("_REDIS_URL") and value:
38            return value
39    return None
Vercel's Redis integration names its variable after the store, so any *_REDIS_URL is accepted.

The leaderboard operation is the one worth reading closely. Redis keeps a sorted set per board; the member is the JSON entry itself (with a random id, so two identical runs never collapse), trimmed to the top 500:

●store.py : 194–200trace ↗
194    def board_add(self, key: str, entry: dict, score: int, keep: int = 500) -> int:
195        # The member carries a unique id so two identical runs don't collapse.
196        member = json.dumps(entry, sort_keys=True)
197        self.r.zadd(key, {member: score})
198        self.r.zremrangebyrank(key, 0, -(keep + 1))
199        rank = cast("int | None", self.r.zrevrank(key, member))
200        return (rank if rank is not None else keep) + 1
board_add on Redis: add, trim to the top `keep`, return the new entry's rank.

The game's own Hall is still local

The game still has its Hall of the Fallen, unchanged: loadHall probes /api/hall, gets 503, and keeps runs in this browser. The shared boards are separate and per tour and rule set; the game hands a finished run to the page around it (chapter 17), and that page posts it to /api/score.

The guide is part of the app

This guide is a second small Flask app mounted under /guide with werkzeug's DispatcherMiddleware. Its pages are written with root-absolute links; an after_request hook prefixes them on the way out, so the guide works unchanged at any mount point:

●runner.py : 714–714trace ↗
714    app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/guide": guide_app})  # ty: ignore[invalid-assignment]
One line mounts the guide inside the game.
Key takeaways
  • One Flask app serves pages, the archive API, boards and this guide, here and on Vercel.
  • Shared state goes through store.py: Redis on Vercel, JSON files in the workspace, same interface.
  • The game's own Hall stays local; shared boards are per tour and rule set, posted by the page around the game.