Skip to content

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

13 Commits

Folders and files

Repository files navigation

Flappy Gull

Multiplayer seagull racing game. Live at https://flappygull.val.run (Val Town val stormfar/flappygull).

It used to be a Next.js app on Vercel with Supabase. It now runs as a single Val Town HTTP val: a Hono server, SQLite (std/sqlite), blob storage for the images, and the original React + Phaser game served to the browser with no build step.

Layout

main.ts                 HTTP entrypoint (Hono): page shell, /api, /assets, frontend modules
backend/
  api.ts                JSON API, replacing the browser's direct Supabase calls and Realtime
  db.ts                 SQLite access + schema (std/sqlite on Val Town, libsql file locally)
  assets.ts             serves /assets/* and /favicon.ico from blob storage (public/ locally)
  shell.ts              the HTML page: import map (esm.sh), Tailwind browser compiler, fonts
  styles.ts             Tailwind v4 source (was app/globals.css)
  static.ts             serves frontend/ modules (std/utils on Val Town, esbuild locally)
frontend/
  index.tsx, App.tsx    entry + client-side router (was app/layout.tsx + the app/ routes)
  pages/                the four Next pages: /, /game/multiplayer, /game/practice, /game/test
  components/           the original lobby and results components
  game/                 the original Phaser game; systems/MultiplayerManager.ts now syncs over HTTP
  lib/api.ts            client for backend/api.ts (replaces lib/supabase.ts)
  lib/types.ts          Match / MatchPlayer / LeaderboardEntry types (were in lib/supabase.ts)
  lib/navigation.ts     drop-in for next/navigation (useRouter / useSearchParams)
dev/server.ts           local dev server, not deployed
public/                 game images (the source for the val's blob storage, see below)
assets/, docs/          source art and the original design docs
scripts/
  sync-assets.ts        copies public/ images from GitHub into the val's blob storage (runs on Val Town)
  export-from-supabase.sql, import-sql.ts   one-off data migration, not deployed

Imports use @/components/..., @/lib/... and @/game. The import map in backend/shell.ts resolves those, plus react, react-dom and uuid from esm.sh and Phaser's ES module build from jsDelivr, pinned to the versions in the old package-lock.json. (Phaser doesn't come from esm.sh because esm.sh builds it from the CommonJS bundle, which has no named exports.)

Environment variables

None. The Next app only needed the Supabase URL and anon key.

How multiplayer works now

Val Town doesn't accept WebSocket connections, so Supabase Realtime is replaced with HTTP:

  • Lobby: the waiting room polls GET /api/matches/:id/state (match + players) every 750 ms instead of subscribing to postgres_changes. Guests see the match start within a second, well inside the 3-second countdown, which is still timed from the match's started_at.
  • In a match: each player POSTs its position to /api/matches/:id/positions about 6 times a second (one request in flight at a time) and gets everyone else's latest position back in the same response. Supabase broadcast sent 20 a second. Ghost gulls are extrapolated between updates (MultiplayerManager.getOtherPlayers), so they still move smoothly. Positions live in the positions table and a player who goes quiet for 5 s disappears.
  • Clock skew: every API response carries the server time. started_at is set by the server (it was the host's clock) and converted to each player's local clock, so countdowns and match timers line up across devices.

Behaviour differences from the Next/Supabase app

  • Ghost position updates are ~6/s with extrapolation instead of 20/s (see above).
  • Joining checks "match full" and duplicates on the server in one statement.
  • "Leave match" actually removes you (the old RLS policies had no DELETE, so it silently failed).
  • Starting a match is limited to the host (first player to join).
  • Pressing Space after a multiplayer match ended used to restart it instantly and submit an extra 0-point score (visible in the old data). Space now does nothing there, and the score sent is the one at the moment the match ended.
  • match_players.final_score / best_flight / finished_at are now filled in when a score is submitted.
  • The beach umbrellas in the lobby background show up: their image URL contains a space and wasn't quoted, so browsers ignored it.

Migration record (2026-11-03)

  • Data copied from the Supabase project flappygull (pennrorgrgastugiifle) with scripts/export-from-supabase.sql, keeping every id: 16 matches, 72 match players, 33 leaderboard entries (score totals verified identical).
  • Images copied into the val's blob storage with scripts/sync-assets.ts.
  • The Vercel project flappygull and the Supabase project are paused; https://flappygull.val.run is the only deployment.

Local development

deno run -A dev/server.ts   # http://localhost:8787

This uses dev/local.db and reads images from public/. To load a Supabase export into it: FLAPPYGULL_LOCAL_DB=dev/local.db deno run -A scripts/import-sql.ts export.sql.

Deploying changes

Upload changed files to the val stormfar/flappygull with the val.town editor or the vt CLI, keeping their paths: main.ts is the HTTP file; everything in backend/ and frontend/, and scripts/sync-assets.ts, are scripts. Don't upload dev/, the other scripts/, or deno.json.

After adding or changing an image in public/, add it to ASSET_FILES in backend/assets.ts, push to GitHub, upload backend/assets.ts and run scripts/sync-assets.ts on Val Town (it copies the images from GitHub into the val's blob storage; set ASSETS_GIT_REF for a branch other than main).

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages