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:
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:
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:
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")
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
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:
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
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:
714 app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/guide": guide_app}) # ty: ignore[invalid-assignment]
- 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.