Files
zooqueenandHanzo Dev 4367112d4f Lux Quest: build a scavenger hunt out of real places and send it to a friend
The repo was a "coming soon" page. This is the game.

A quest is a VALUE, not a database row — name, prize and every stop encode into
the share link, so making one and sending it needs no account, no server and no
storage. The code rides in the URL FRAGMENT, never the query string: a fragment
is not sent to the server and does not appear in a Referer header, so the places
someone chose stay out of access logs and off any site they click through to.
`?q=` would quietly publish every quest anyone shares.

Arriving is the one rule worth getting right. A stop is found when the device
reports a position inside its radius, and the browser's own accuracy estimate is
ADDED to that radius — a phone between tall buildings reports ±50 m, and holding
it to the bare radius tells someone standing at the right door they are not
there. Wildly inaccurate readings are capped, so a device claiming ±50 km cannot
unlock a city from the sofa.

Google is confined to one module. With no NEXT_PUBLIC_GOOGLE_MAPS_KEY every stop
falls back to a list with a Maps link and the hunt is still playable, and "no key
configured" is kept distinct from "Google refused" — collapsing those is how a
missing env var gets mistaken for an outage.

Prizes are the maker's own words, settled between the two people. Nothing here
holds money or hands anything out; an escrow would be a custody problem and
saying otherwise in the UI would be a lie about what happens when someone wins.

Proven, not asserted: 28 unit tests over the pure core (unicode place names,
truncated and hand-edited links returning null instead of throwing, coordinate
precision against a 45 m radius, corrupt localStorage) and 7 Playwright tests
driving the real static export at phone size with FAKED GPS — walk to Clarion
Alley and the point lands, stand three blocks away and it does not, refuse
location and the screen says so rather than doing nothing silently.

Also fixes a pre-existing break: Tailwind was configured with no stylesheet
importing it, so every utility class in the app was inert.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 16:27:59 -07:00

3.0 KiB

Lux Quest

Lux Quest

Build a scavenger hunt out of real places and send it to a friend. They walk the city to solve it — clue by clue, checking in when they arrive.

pnpm install
pnpm dev          # http://localhost:3000
pnpm test         # unit — codec, distance maths, saved progress
pnpm e2e          # browser — the whole game loop, with faked GPS
pnpm build        # static export to out/

How it works

A quest is a value, not a database row. The name, the prize and every stop encode into the share link, so making one and sending it needs no account, no server and no storage. There is nothing to sign up for and no copy is kept.

The code rides in the URL fragment (/play#…), never the query string. A fragment is not sent to the server and does not appear in a Referer header, so the places someone chose stay out of access logs and off any site they click through to. Moving it to ?q= would quietly publish every quest anyone shares.

Play progress is localStorage, keyed by a hash of the share code — so two quests never collide, and editing one starts a clean run rather than inheriting the old run's finds.

Arriving

A stop is found when the device reports a position within its radius. The browser's own accuracy estimate is added to that radius: a phone between tall buildings routinely reports ±50 m, and holding it to the bare radius tells someone standing at the right door that they are not there. Wildly inaccurate readings are capped, so a device claiming ±50 km cannot unlock a city from the sofa.

Google Maps

Set NEXT_PUBLIC_GOOGLE_MAPS_KEY to render the map. Without it the app still works: every stop falls back to a list with a Google Maps link, so a hunt is playable with no key at all. The two failure modes are told apart on purpose — no key configured is not the same as Google refused, and collapsing them is how a missing environment variable gets mistaken for an outage.

Prizes

Whatever the maker offers, in their own words, settled directly between the two of them. Lux Quest holds no money, takes no payment and hands nothing out — anything else is a custody problem, and pretending otherwise in the UI would be a lie about what happens when someone wins.

Layout

src/lib/quest.ts     the value + its codec        pure
src/lib/geo.ts       distance, and "am I there"   pure
src/lib/progress.ts  what this device has found   localStorage
src/lib/maps.ts      the only module that knows Google exists
src/components/      the map, and its no-key fallback
src/app/             landing · /create · /play

The pure modules hold every rule worth arguing about, which is why they are tested without a browser, a network or a device. The e2e suite fakes geolocation rather than stubbing the app, so the production navigator.geolocation path and the real arrival maths both run: a unit test cannot prove that someone standing at Clarion Alley gets their point.

Built on Next 15 (static export), Tailwind, @luxfi/ui.